Merge branch 'develop' into fix/notification-comply-with-upstream

This commit is contained in:
mergify[bot] 2024-01-28 05:42:37 +00:00 committed by GitHub
commit 6a63a8997d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
516 changed files with 1477301 additions and 575980 deletions

View File

@ -28,4 +28,7 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
494bd9ef78313436f0424b918f200dab8fc7c20b 494bd9ef78313436f0424b918f200dab8fc7c20b
# bulk format python code with black # bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d baec607ff5905b1c67531096a9cf50ec7ff00a5d
# bulk refactor with sourcery
eb9ee3f79b94e594fc6dfa4f6514580e125eee8c

View File

@ -21,6 +21,6 @@ jobs:
- name: Run backport - name: Run backport
uses: ./actions/backport uses: ./actions/backport
with: with:
token: ${{secrets.BACKPORT_BOT_TOKEN}} token: ${{secrets.RELEASE_TOKEN}}
labelsToAdd: "backport" labelsToAdd: "backport"
title: "{{originalTitle}}" title: "{{originalTitle}}"

View File

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
version: ["13", "14", "15"] version: ["14", "15"]
steps: steps:
- uses: octokit/request-action@v2.x - uses: octokit/request-action@v2.x

View File

@ -20,6 +20,18 @@ jobs:
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0 uses: pre-commit/action@v3.0.0
semgrep:
name: semgrep
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: pip
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules

21
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: 'Lock threads'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 14
pr-inactive-days: 14

22
.github/workflows/patch_faux.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Patch Test
on:
pull_request:
paths:
- "**.js"
- "**.css"
- "**.md"
- "**.html"
- "**.csv"
jobs:
test:
runs-on: ubuntu-latest
name: Patch Test
steps:
- name: Pass skipped tests unconditionally
run: "echo Skipped"

View File

@ -16,7 +16,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 20
- name: Setup dependencies - name: Setup dependencies
run: | run: |
npm install @semantic-release/git @semantic-release/exec --no-save npm install @semantic-release/git @semantic-release/exec --no-save

View File

@ -0,0 +1,24 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Tests
on:
pull_request:
paths:
- "**.js"
- "**.css"
- "**.md"
- "**.html"
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
container: [1, 2, 3, 4]
name: Python Unit Tests
steps:
- name: Pass skipped tests unconditionally
run: "echo Skipped"

View File

@ -5,7 +5,7 @@ fail_fast: false
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.3.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
files: "erpnext.*" files: "erpnext.*"
@ -15,6 +15,10 @@ repos:
args: ['--branch', 'develop'] args: ['--branch', 'develop']
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/pre-commit/mirrors-eslint - repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0 rev: v8.44.0

3
crowdin.yml Normal file
View File

@ -0,0 +1,3 @@
files:
- source: /erpnext/locale/main.pot
translation: /erpnext/locale/%two_letters_code%.po

View File

@ -36,7 +36,7 @@ def get_default_cost_center(company):
if not frappe.flags.company_cost_center: if not frappe.flags.company_cost_center:
frappe.flags.company_cost_center = {} frappe.flags.company_cost_center = {}
if not company in frappe.flags.company_cost_center: if company not in frappe.flags.company_cost_center:
frappe.flags.company_cost_center[company] = frappe.get_cached_value( frappe.flags.company_cost_center[company] = frappe.get_cached_value(
"Company", company, "cost_center" "Company", company, "cost_center"
) )
@ -47,7 +47,7 @@ def get_company_currency(company):
"""Returns the default company currency""" """Returns the default company currency"""
if not frappe.flags.company_currency: if not frappe.flags.company_currency:
frappe.flags.company_currency = {} frappe.flags.company_currency = {}
if not company in frappe.flags.company_currency: if company not in frappe.flags.company_currency:
frappe.flags.company_currency[company] = frappe.db.get_value( frappe.flags.company_currency[company] = frappe.db.get_value(
"Company", company, "default_currency", cache=True "Company", company, "default_currency", cache=True
) )
@ -81,7 +81,7 @@ def is_perpetual_inventory_enabled(company):
if not hasattr(frappe.local, "enable_perpetual_inventory"): if not hasattr(frappe.local, "enable_perpetual_inventory"):
frappe.local.enable_perpetual_inventory = {} frappe.local.enable_perpetual_inventory = {}
if not company in frappe.local.enable_perpetual_inventory: if company not in frappe.local.enable_perpetual_inventory:
frappe.local.enable_perpetual_inventory[company] = ( frappe.local.enable_perpetual_inventory[company] = (
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0 frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
) )
@ -96,7 +96,7 @@ def get_default_finance_book(company=None):
if not hasattr(frappe.local, "default_finance_book"): if not hasattr(frappe.local, "default_finance_book"):
frappe.local.default_finance_book = {} frappe.local.default_finance_book = {}
if not company in frappe.local.default_finance_book: if company not in frappe.local.default_finance_book:
frappe.local.default_finance_book[company] = frappe.get_cached_value( frappe.local.default_finance_book[company] = frappe.get_cached_value(
"Company", company, "default_finance_book" "Company", company, "default_finance_book"
) )
@ -108,7 +108,7 @@ def get_party_account_type(party_type):
if not hasattr(frappe.local, "party_account_types"): if not hasattr(frappe.local, "party_account_types"):
frappe.local.party_account_types = {} frappe.local.party_account_types = {}
if not party_type in frappe.local.party_account_types: if party_type not in frappe.local.party_account_types:
frappe.local.party_account_types[party_type] = ( frappe.local.party_account_types[party_type] = (
frappe.db.get_value("Party Type", party_type, "account_type") or "" frappe.db.get_value("Party Type", party_type, "account_type") or ""
) )

View File

@ -232,7 +232,7 @@ def calculate_monthly_amount(
if amount + already_booked_amount_in_account_currency > item.net_amount: if amount + already_booked_amount_in_account_currency > item.net_amount:
amount = item.net_amount - already_booked_amount_in_account_currency amount = item.net_amount - already_booked_amount_in_account_currency
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date): if get_first_day(start_date) != start_date or get_last_day(end_date) != end_date:
partial_month = flt(date_diff(end_date, start_date)) / flt( partial_month = flt(date_diff(end_date, start_date)) / flt(
date_diff(get_last_day(end_date), get_first_day(start_date)) date_diff(get_last_day(end_date), get_first_day(start_date))
) )

View File

@ -108,6 +108,7 @@
"fieldname": "parent_account", "fieldname": "parent_account",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"in_preview": 1,
"label": "Parent Account", "label": "Parent Account",
"oldfieldname": "parent_account", "oldfieldname": "parent_account",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@ -192,7 +193,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2023-07-20 18:18:44.405723", "modified": "2024-01-10 04:57:33.681676",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account", "name": "Account",
@ -249,8 +250,9 @@
], ],
"search_fields": "account_number", "search_fields": "account_number",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"show_preview_popup": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -91,8 +91,8 @@ class Account(NestedSet):
super(Account, self).on_update() super(Account, self).on_update()
def onload(self): def onload(self):
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles(): if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True) self.set_onload("can_freeze_account", True)

View File

@ -77,7 +77,7 @@ frappe.treeview_settings["Account"] = {
// show Dr if positive since balance is calculated as debit - credit else show Cr // show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance; const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr"; const dr_or_cr = balance > 0 ? __("Dr"): __("Cr");
const format = (value, currency) => format_currency(Math.abs(value), currency); const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { if (account.balance!==undefined) {

View File

@ -74,7 +74,7 @@ def create_charts(
# after all accounts are already inserted. # after all accounts are already inserted.
frappe.local.flags.ignore_update_nsm = True frappe.local.flags.ignore_update_nsm = True
_import_accounts(chart, None, None, root_account=True) _import_accounts(chart, None, None, root_account=True)
rebuild_tree("Account", "parent_account") rebuild_tree("Account")
frappe.local.flags.ignore_update_nsm = False frappe.local.flags.ignore_update_nsm = False
@ -231,6 +231,8 @@ def build_account_tree(tree, parent, all_accounts):
tree[child.account_name]["account_type"] = child.account_type tree[child.account_name]["account_type"] = child.account_type
if child.tax_rate: if child.tax_rate:
tree[child.account_name]["tax_rate"] = child.tax_rate tree[child.account_name]["tax_rate"] = child.tax_rate
if child.account_currency:
tree[child.account_name]["account_currency"] = child.account_currency
if not parent: if not parent:
tree[child.account_name]["root_type"] = child.root_type tree[child.account_name]["root_type"] = child.root_type

View File

@ -26,7 +26,7 @@
"0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"}, "0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"},
"0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"}, "0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"},
"0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"}, "0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"},
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"}, "0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
"0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"}, "0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"},
"0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"}, "0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"},
"0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"}, "0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"},
@ -65,42 +65,41 @@
"0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"}, "0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"},
"0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"}, "0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"},
"root_type": "Asset" "root_type": "Asset"
}, },
"Klasse 1 Aktiva: Vorr\u00e4te": { "Klasse 1 Aktiva: Vorr\u00e4te": {
"1000 Bezugsverrechnung": {"account_type": "Stock"}, "1000 Bezugsverrechnung": {"account_type": "Stock"},
"1100 Rohstoffe": {"account_type": "Stock"}, "1100 Rohstoffe": {"account_type": "Stock"},
"1200 Bezogene Teile": {"account_type": "Stock"}, "1200 Bezogene Teile": {"account_type": "Stock"},
"1300 Hilfsstoffe": {"account_type": "Stock"}, "1300 Hilfsstoffe": {"account_type": "Stock"},
"1350 Betriebsstoffe": {"account_type": "Stock"}, "1350 Betriebsstoffe": {"account_type": "Stock"},
"1360 Vorrat Energietraeger": {"account_type": "Stock"}, "1360 Vorrat Energietraeger": {"account_type": "Stock"},
"1400 Unfertige Erzeugnisse": {"account_type": "Stock"}, "1400 Unfertige Erzeugnisse": {"account_type": "Stock"},
"1500 Fertige Erzeugnisse": {"account_type": "Stock"}, "1500 Fertige Erzeugnisse": {"account_type": "Stock"},
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"}, "1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"},
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"}, "1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"},
"1800 Geleistete Anzahlungen": {"account_type": "Stock"}, "1800 Geleistete Anzahlungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"}, "1900 Wertberichtigungen": {"account_type": "Stock"},
"root_type": "Asset" "root_type": "Asset"
}, },
"Klasse 3 Passiva: Verbindlichkeiten": { "Klasse 3 Passiva: Verbindlichkeiten": {
"3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"}, "3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"},
"3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"}, "3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"},
"3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"}, "3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"},
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"}, "3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
"3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"}, "3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"},
"3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"}, "3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"},
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"}, "3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
"3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": { "3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
"account_type": "Payable" "account_type": "Payable"
}, },
"3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {}, "3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
"3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"}, "3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"},
"3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"}, "3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"},
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"}, "3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"}, "3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"}, "3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
"3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"}, "3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"},
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"}, "3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
"3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"}, "3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"},
"3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"}, "3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"},
"3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"}, "3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"},
@ -119,13 +118,13 @@
}, },
"3515 Umsatzsteuer Inland 10%": { "3515 Umsatzsteuer Inland 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3520 Umsatzsteuer aus i.g. Erwerb 20%": { "3520 Umsatzsteuer aus i.g. Erwerb 20%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3525 Umsatzsteuer aus i.g. Erwerb 10%": { "3525 Umsatzsteuer aus i.g. Erwerb 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {}, "3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
"3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": { "3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
"account_type": "Payable" "account_type": "Payable"
@ -141,7 +140,7 @@
"account_type": "Tax" "account_type": "Tax"
}, },
"root_type": "Liability" "root_type": "Liability"
}, },
"Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": { "Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": {
"2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": { "2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": {
"account_type": "Receivable" "account_type": "Receivable"
@ -154,7 +153,7 @@
}, },
"2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": { "2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2100 Forderungen aus Lieferungen und Leistungen EU": { "2100 Forderungen aus Lieferungen und Leistungen EU": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
@ -192,7 +191,7 @@
"account_type": "Receivable" "account_type": "Receivable"
}, },
"2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"}, "2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"},
"2460 Eingeforderte aber noch nicht eingezahlte Einlagen": { "2460 Eingeforderte aber noch nicht eingezahlte Einlagen": {
"account_type": "Receivable" "account_type": "Receivable"
}, },
@ -243,10 +242,10 @@
}, },
"2800 Guthaben bei Bank": { "2800 Guthaben bei Bank": {
"account_type": "Bank" "account_type": "Bank"
}, },
"2801 Guthaben bei Bank - Sparkonto": { "2801 Guthaben bei Bank - Sparkonto": {
"account_type": "Bank" "account_type": "Bank"
}, },
"2810 Guthaben bei Paypal": { "2810 Guthaben bei Paypal": {
"account_type": "Bank" "account_type": "Bank"
}, },
@ -264,19 +263,19 @@
}, },
"2895 Schwebende Geldbewegugen": { "2895 Schwebende Geldbewegugen": {
"account_type": "Bank" "account_type": "Bank"
}, },
"2513 Vorsteuer Inland 5%": { "2513 Vorsteuer Inland 5%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2515 Vorsteuer Inland 20%": { "2515 Vorsteuer Inland 20%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": { "2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": { "2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": {
"account_type": "Tax" "account_type": "Tax"
}, },
"2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": { "2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
"account_type": "Tax" "account_type": "Tax"
}, },
@ -286,16 +285,16 @@
"root_type": "Asset" "root_type": "Asset"
}, },
"Klasse 4: Betriebliche Erträge": { "Klasse 4: Betriebliche Erträge": {
"4000 Erlöse 20 %": {"account_type": "Income Account"}, "4000 Erlöse 20 %": {"account_type": "Income Account"},
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"}, "4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
"4010 Erl\u00f6se 10 %": {"account_type": "Income Account"}, "4010 Erl\u00f6se 10 %": {"account_type": "Income Account"},
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"}, "4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"}, "4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"}, "4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
"4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"}, "4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"},
"4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"}, "4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"},
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"}, "4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"}, "4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
"4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"}, "4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"},
"4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"}, "4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"},
"4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"}, "4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
@ -304,15 +303,15 @@
"4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"}, "4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"},
"4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"}, "4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"},
"root_type": "Income" "root_type": "Income"
}, },
"Klasse 5: Aufwand f\u00fcr Material und Leistungen": { "Klasse 5: Aufwand f\u00fcr Material und Leistungen": {
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"}, "5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
"5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"}, "5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"},
"5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"}, "5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"},
"5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"}, "5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"},
"5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"}, "5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"},
"5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"}, "5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"},
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"}, "5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
"5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"}, "5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"},
"5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"}, "5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"},
"5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"}, "5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"},
@ -340,7 +339,7 @@
"6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"}, "6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"},
"6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"}, "6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"},
"root_type": "Expense" "root_type": "Expense"
}, },
"Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": { "Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": {
"7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"}, "7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"},
"7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"}, "7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"},
@ -349,7 +348,7 @@
"7310 Fahrrad - Aufwand": {"account_type": "Expense Account"}, "7310 Fahrrad - Aufwand": {"account_type": "Expense Account"},
"7320 Kfz - Aufwand": {"account_type": "Expense Account"}, "7320 Kfz - Aufwand": {"account_type": "Expense Account"},
"7330 LKW - Aufwand": {"account_type": "Expense Account"}, "7330 LKW - Aufwand": {"account_type": "Expense Account"},
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"}, "7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
"7350 Reise- und Fahraufwand": {"account_type": "Expense Account"}, "7350 Reise- und Fahraufwand": {"account_type": "Expense Account"},
"7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"}, "7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"},
"7380 Nachrichtenaufwand": {"account_type": "Expense Account"}, "7380 Nachrichtenaufwand": {"account_type": "Expense Account"},
@ -409,7 +408,7 @@
"8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"}, "8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"},
"8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"}, "8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"},
"root_type": "Income" "root_type": "Income"
}, },
"Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": { "Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": {
"9000 Gezeichnetes bzw. gewidmetes Kapital": { "9000 Gezeichnetes bzw. gewidmetes Kapital": {
"account_type": "Equity" "account_type": "Equity"
@ -435,5 +434,5 @@
}, },
"root_type": "Equity" "root_type": "Equity"
} }
} }
} }

View File

@ -33,7 +33,9 @@
}, },
"Stocks": { "Stocks": {
"Mati\u00e8res premi\u00e8res": {}, "Mati\u00e8res premi\u00e8res": {},
"Stock de produits fini": {}, "Stock de produits fini": {
"account_type": "Stock"
},
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {}, "Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
"Travaux en cours": {}, "Travaux en cours": {},
"account_type": "Stock" "account_type": "Stock"
@ -395,9 +397,11 @@
}, },
"Produits": { "Produits": {
"Revenus de ventes": { "Revenus de ventes": {
" Escomptes de volume sur ventes": {}, "Escomptes de volume sur ventes": {},
"Autres produits d'exploitation": {}, "Autres produits d'exploitation": {},
"Ventes": {}, "Ventes": {
"account_type": "Income Account"
},
"Ventes avec des provinces harmonis\u00e9es": {}, "Ventes avec des provinces harmonis\u00e9es": {},
"Ventes avec des provinces non-harmonis\u00e9es": {}, "Ventes avec des provinces non-harmonis\u00e9es": {},
"Ventes \u00e0 l'\u00e9tranger": {} "Ventes \u00e0 l'\u00e9tranger": {}

View File

@ -53,8 +53,13 @@
}, },
"II. Forderungen und sonstige Vermögensgegenstände": { "II. Forderungen und sonstige Vermögensgegenstände": {
"is_group": 1, "is_group": 1,
"Ford. a. Lieferungen und Leistungen": { "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1400", "account_number": "1400",
"account_type": "Receivable",
"is_group": 1
},
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1410",
"account_type": "Receivable" "account_type": "Receivable"
}, },
"Durchlaufende Posten": { "Durchlaufende Posten": {
@ -180,8 +185,13 @@
}, },
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": { "IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
"is_group": 1, "is_group": 1,
"Verbindlichkeiten aus Lieferungen u. Leistungen": { "Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1600", "account_number": "1600",
"account_type": "Payable",
"is_group": 1
},
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1610",
"account_type": "Payable" "account_type": "Payable"
} }
}, },

View File

@ -36,16 +36,16 @@
} }
}, },
"Fixed Assets": { "Fixed Assets": {
"Capital Equipments": { "Capital Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Electronic Equipments": { "Electronic Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Furnitures and Fixtures": { "Furniture and Fixtures": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Office Equipments": { "Office Equipment": {
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Plants and Machineries": { "Plants and Machineries": {

View File

@ -23,13 +23,13 @@ def get():
_("Tax Assets"): {"is_group": 1}, _("Tax Assets"): {"is_group": 1},
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset"}, _("Capital Equipment"): {"account_type": "Fixed Asset"},
_("Electronic Equipments"): {"account_type": "Fixed Asset"}, _("Electronic Equipment"): {"account_type": "Fixed Asset"},
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset"}, _("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
_("Office Equipments"): {"account_type": "Fixed Asset"}, _("Office Equipment"): {"account_type": "Fixed Asset"},
_("Plants and Machineries"): {"account_type": "Fixed Asset"}, _("Plants and Machineries"): {"account_type": "Fixed Asset"},
_("Buildings"): {"account_type": "Fixed Asset"}, _("Buildings"): {"account_type": "Fixed Asset"},
_("Softwares"): {"account_type": "Fixed Asset"}, _("Software"): {"account_type": "Fixed Asset"},
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"}, _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
_("CWIP Account"): { _("CWIP Account"): {
"account_type": "Capital Work in Progress", "account_type": "Capital Work in Progress",

View File

@ -36,13 +36,13 @@ def get():
"account_number": "1100-1600", "account_number": "1100-1600",
}, },
_("Fixed Assets"): { _("Fixed Assets"): {
_("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"}, _("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
_("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"}, _("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, _("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
_("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"}, _("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"}, _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"}, _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
_("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"}, _("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
_("Accumulated Depreciation"): { _("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation", "account_type": "Accumulated Depreciation",
"account_number": "1780", "account_number": "1780",

View File

@ -119,7 +119,7 @@ class TestAccount(unittest.TestCase):
InvalidAccountMergeError, InvalidAccountMergeError,
merge_account, merge_account,
"Capital Stock - _TC", "Capital Stock - _TC",
"Softwares - _TC", "Software - _TC",
) )
# Raise error as currency doesn't match # Raise error as currency doesn't match

View File

@ -11,6 +11,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
@ -19,7 +20,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-01 12:32:34.044911", "modified": "2024-01-03 11:13:02.669632",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Allowed To Transact With", "name": "Allowed To Transact With",
@ -28,5 +29,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -55,7 +55,7 @@ class BankAccount(Document):
def validate_company(self): def validate_company(self):
if self.is_company_account and not self.company: if self.is_company_account and not self.company:
frappe.throw(_("Company is manadatory for company account")) frappe.throw(_("Company is mandatory for company account"))
def validate_iban(self): def validate_iban(self):
""" """

View File

@ -48,11 +48,11 @@ class BankGuarantee(Document):
def on_submit(self): def on_submit(self):
if not self.bank_guarantee_number: if not self.bank_guarantee_number:
frappe.throw(_("Enter the Bank Guarantee Number before submittting.")) frappe.throw(_("Enter the Bank Guarantee Number before submitting."))
if not self.name_of_beneficiary: if not self.name_of_beneficiary:
frappe.throw(_("Enter the name of the Beneficiary before submittting.")) frappe.throw(_("Enter the name of the Beneficiary before submitting."))
if not self.bank: if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) frappe.throw(_("Enter the name of the bank or lending institution before submitting."))
@frappe.whitelist() @frappe.whitelist()

View File

@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: { args: {
bank_account: frm.doc.bank_account, bank_account: frm.doc.bank_account,
till_date: frm.doc.bank_statement_from_date, till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
}, },
callback: (response) => { callback: (response) => {
frm.set_value("account_opening_balance", response.message); frm.set_value("account_opening_balance", response.message);

View File

@ -444,6 +444,10 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers) transaction.add_payment_entries(vouchers)
transaction.validate_duplicate_references()
transaction.allocate_payment_entries()
transaction.update_allocated_amount()
transaction.set_status()
transaction.save() transaction.save()
return transaction return transaction

View File

@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
"deposit": 100, "deposit": 100,
"bank_account": self.bank_account, "bank_account": self.bank_account,
"reference_number": "123", "reference_number": "123",
"currency": "INR",
} }
) )
.save() .save()

View File

@ -3,12 +3,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(Document):
class BankTransaction(StatusUpdater):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@ -49,6 +49,33 @@ class BankTransaction(StatusUpdater):
def validate(self): def validate(self):
self.validate_duplicate_references() self.validate_duplicate_references()
self.validate_currency()
def validate_currency(self):
"""
Bank Transaction should be on the same currency as the Bank Account.
"""
if self.currency and self.bank_account:
account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
account_currency = frappe.get_cached_value("Account", account, "account_currency")
if self.currency != account_currency:
frappe.throw(
_(
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
).format(
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
)
)
def set_status(self):
if self.docstatus == 2:
self.db_set("status", "Cancelled")
elif self.docstatus == 1:
if self.unallocated_amount > 0:
self.db_set("status", "Unreconciled")
elif self.unallocated_amount <= 0:
self.db_set("status", "Reconciled")
def validate_duplicate_references(self): def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction""" """Make sure the same voucher is not allocated twice within the same Bank Transaction"""
@ -83,12 +110,13 @@ class BankTransaction(StatusUpdater):
self.validate_duplicate_references() self.validate_duplicate_references()
self.allocate_payment_entries() self.allocate_payment_entries()
self.update_allocated_amount() self.update_allocated_amount()
self.set_status()
def on_cancel(self): def on_cancel(self):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True) self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.set_status(update=True) self.set_status()
def add_payment_entries(self, vouchers): def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance" "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
@ -366,15 +394,17 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
and len(get_reconciled_bank_transactions(doctype, docname)) < 2 and len(get_reconciled_bank_transactions(doctype, docname)) < 2
): ):
return return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice": if doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=doctype, parent=docname), dict(parenttype=doctype, parent=docname),
"clearance_date", "clearance_date",
clearance_date, clearance_date,
) )
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Bank Transaction": elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund # For when a second bank transaction has fixed another, e.g. refund
@ -404,3 +434,21 @@ def unclear_reference_payment(doctype, docname, bt_name):
bt = frappe.get_doc("Bank Transaction", bt_name) bt = frappe.get_doc("Bank Transaction", bt_name)
set_voucher_clearance(doctype, docname, None, bt) set_voucher_clearance(doctype, docname, None, bt)
return docname return docname
def remove_from_bank_transaction(doctype, docname):
"""Remove a (cancelled) voucher from all Bank Transactions."""
for bt_name in get_reconciled_bank_transactions(doctype, docname):
bt = frappe.get_doc("Bank Transaction", bt_name)
if bt.docstatus == DocStatus.cancelled():
continue
modified = False
for pe in bt.payment_entries:
if pe.payment_document == doctype and pe.payment_entry == docname:
bt.remove(pe)
modified = True
if modified:
bt.save()

View File

@ -2,10 +2,10 @@
# See license.txt # See license.txt
import json import json
import unittest
import frappe import frappe
from frappe import utils from frappe import utils
from frappe.model.docstatus import DocStatus
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@ -81,6 +81,29 @@ class TestBankTransaction(FrappeTestCase):
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertFalse(clearance_date) self.assertFalse(clearance_date)
def test_cancel_voucher(self):
bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers)
payment.reload()
payment.cancel()
bank_transaction.reload()
self.assertEqual(bank_transaction.docstatus, DocStatus.submitted())
self.assertEqual(bank_transaction.unallocated_amount, 1700)
self.assertEqual(bank_transaction.payment_entries, [])
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self): def test_debit_credit_output(self):
bank_transaction = frappe.get_doc( bank_transaction = frappe.get_doc(

View File

@ -0,0 +1,100 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Bisect Accounting Statements", {
onload(frm) {
frm.trigger("render_heatmap");
},
refresh(frm) {
frm.add_custom_button(__('Bisect Left'), () => {
frm.trigger("bisect_left");
});
frm.add_custom_button(__('Bisect Right'), () => {
frm.trigger("bisect_right");
});
frm.add_custom_button(__('Up'), () => {
frm.trigger("move_up");
});
frm.add_custom_button(__('Build Tree'), () => {
frm.trigger("build_tree");
});
},
render_heatmap(frm) {
let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
bisect_heatmap.addClass("bisect_heatmap_location");
// milliseconds in a day
let msiad=24*60*60*1000;
let datapoints = {};
let fr_dt = new Date(frm.doc.from_date).getTime();
let to_dt = new Date(frm.doc.to_date).getTime();
let bisect_start = new Date(frm.doc.current_from_date).getTime();
let bisect_end = new Date(frm.doc.current_to_date).getTime();
for(let x=fr_dt; x <= to_dt; x+=msiad){
let epoch_in_seconds = x/1000;
if ((bisect_start <= x) && (x <= bisect_end )) {
datapoints[epoch_in_seconds] = 1.0;
} else {
datapoints[epoch_in_seconds] = 0.0;
}
}
new frappe.Chart(".bisect_heatmap_location", {
type: "heatmap",
data: {
dataPoints: datapoints,
start: new Date(frm.doc.from_date),
end: new Date(frm.doc.to_date),
},
countLabel: 'Bisecting',
discreteDomains: 1,
});
},
bisect_left(frm) {
frm.call({
doc: frm.doc,
method: 'bisect_left',
freeze: true,
freeze_message: __("Bisecting Left ..."),
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
bisect_right(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Bisecting Right ..."),
method: 'bisect_right',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
move_up(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Moving up in tree ..."),
method: 'move_up',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
build_tree(frm) {
frm.call({
doc: frm.doc,
freeze: true,
freeze_message: __("Rebuilding BTree for period ..."),
method: 'build_tree',
callback: (r) => {
frm.trigger("render_heatmap");
}
});
},
});

View File

@ -0,0 +1,194 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-09-15 21:28:28.054773",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_cvfg",
"company",
"column_break_hcam",
"from_date",
"column_break_qxbi",
"to_date",
"column_break_iwny",
"algorithm",
"section_break_8ph9",
"current_node",
"section_break_ngid",
"bisect_heatmap",
"section_break_hmsy",
"bisecting_from",
"current_from_date",
"column_break_uqyd",
"bisecting_to",
"current_to_date",
"section_break_hbyo",
"heading_cppb",
"p_l_summary",
"column_break_aivo",
"balance_sheet_summary",
"b_s_summary",
"column_break_gvwx",
"difference_heading",
"difference"
],
"fields": [
{
"fieldname": "column_break_qxbi",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Datetime",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Datetime",
"label": "To Date"
},
{
"default": "BFS",
"fieldname": "algorithm",
"fieldtype": "Select",
"label": "Algorithm",
"options": "BFS\nDFS"
},
{
"fieldname": "column_break_iwny",
"fieldtype": "Column Break"
},
{
"fieldname": "current_node",
"fieldtype": "Link",
"label": "Current Node",
"options": "Bisect Nodes"
},
{
"fieldname": "section_break_hmsy",
"fieldtype": "Section Break"
},
{
"fieldname": "current_from_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "current_to_date",
"fieldtype": "Datetime",
"read_only": 1
},
{
"fieldname": "column_break_uqyd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_hbyo",
"fieldtype": "Section Break"
},
{
"fieldname": "p_l_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "b_s_summary",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "difference",
"fieldtype": "Float",
"read_only": 1
},
{
"fieldname": "column_break_aivo",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_gvwx",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "column_break_hcam",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngid",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_8ph9",
"fieldtype": "Section Break",
"hidden": 1
},
{
"fieldname": "bisect_heatmap",
"fieldtype": "HTML",
"label": "Heatmap"
},
{
"fieldname": "heading_cppb",
"fieldtype": "Heading",
"label": "Profit and Loss Summary"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Heading",
"label": "Balance Sheet Summary"
},
{
"fieldname": "difference_heading",
"fieldtype": "Heading",
"label": "Difference"
},
{
"fieldname": "bisecting_from",
"fieldtype": "Heading",
"label": "Bisecting From"
},
{
"fieldname": "bisecting_to",
"fieldtype": "Heading",
"label": "Bisecting To"
},
{
"fieldname": "section_break_cvfg",
"fieldtype": "Section Break"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-12-01 16:49:54.073890",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Accounting Statements",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,226 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import datetime
from collections import deque
from math import floor
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from frappe.utils.data import guess_date_format
class BisectAccountingStatements(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
algorithm: DF.Literal["BFS", "DFS"]
b_s_summary: DF.Float
company: DF.Link | None
current_from_date: DF.Datetime | None
current_node: DF.Link | None
current_to_date: DF.Datetime | None
difference: DF.Float
from_date: DF.Datetime | None
p_l_summary: DF.Float
to_date: DF.Datetime | None
# end: auto-generated types
def validate(self):
self.validate_dates()
def validate_dates(self):
if getdate(self.from_date) > getdate(self.to_date):
frappe.throw(
_("From Date: {0} cannot be greater than To date: {1}").format(
frappe.bold(self.from_date), frappe.bold(self.to_date)
)
)
def bfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_queue = deque([node])
while period_queue:
cur_node = period_queue.popleft()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_queue.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_queue.append(right_node)
cur_node.save()
def dfs(self, from_date: datetime, to_date: datetime):
# Make Root node
node = frappe.new_doc("Bisect Nodes")
node.root = None
node.period_from_date = from_date
node.period_to_date = to_date
node.insert()
period_stack = [node]
while period_stack:
cur_node = period_stack.pop()
delta = cur_node.period_to_date - cur_node.period_from_date
if delta.days == 0:
continue
else:
cur_floor = floor(delta.days / 2)
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
left_node = frappe.new_doc("Bisect Nodes")
left_node.period_from_date = cur_node.period_from_date
left_node.period_to_date = next_to_date
left_node.root = cur_node.name
left_node.generated = False
left_node.insert()
cur_node.left_child = left_node.name
period_stack.append(left_node)
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
right_node = frappe.new_doc("Bisect Nodes")
right_node.period_from_date = next_from_date
right_node.period_to_date = cur_node.period_to_date
right_node.root = cur_node.name
right_node.generated = False
right_node.insert()
cur_node.right_child = right_node.name
period_stack.append(right_node)
cur_node.save()
@frappe.whitelist()
def build_tree(self):
frappe.db.delete("Bisect Nodes")
# Convert str to datetime format
dt_format = guess_date_format(self.from_date)
from_date = datetime.datetime.strptime(self.from_date, dt_format)
to_date = datetime.datetime.strptime(self.to_date, dt_format)
if self.algorithm == "BFS":
self.bfs(from_date, to_date)
if self.algorithm == "DFS":
self.dfs(from_date, to_date)
# set root as current node
root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
self.get_report_summary()
self.current_node = root.name
self.current_from_date = self.from_date
self.current_to_date = self.to_date
self.save()
def get_report_summary(self):
filters = {
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": self.current_from_date,
"period_end_date": self.current_to_date,
"periodicity": "Yearly",
}
pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
bs_summary = frappe.get_doc("Report", "Balance Sheet")
self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
self.difference = abs(self.p_l_summary - self.b_s_summary)
def update_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
current_node.balance_sheet_summary = self.b_s_summary
current_node.profit_loss_summary = self.p_l_summary
current_node.difference = self.difference
current_node.generated = True
current_node.save()
def current_node_has_summary_info(self):
"Assertion method"
return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
def fetch_summary_info_from_current_node(self):
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
self.p_l_summary = current_node.balance_sheet_summary
self.b_s_summary = current_node.profit_loss_summary
self.difference = abs(self.p_l_summary - self.b_s_summary)
def fetch_or_calculate(self):
if self.current_node_has_summary_info():
self.fetch_summary_info_from_current_node()
else:
self.get_report_summary()
self.update_node()
@frappe.whitelist()
def bisect_left(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.left_child is not None:
lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
self.current_node = cur_node.left_child
self.current_from_date = lft_node.period_from_date
self.current_to_date = lft_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Left"))
@frappe.whitelist()
def bisect_right(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.right_child is not None:
rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
self.current_node = cur_node.right_child
self.current_from_date = rgt_node.period_from_date
self.current_to_date = rgt_node.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("No more children on Right"))
@frappe.whitelist()
def move_up(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
if cur_node.root is not None:
root = frappe.get_doc("Bisect Nodes", cur_node.root)
self.current_node = cur_node.root
self.current_from_date = root.period_from_date
self.current_to_date = root.period_to_date
self.fetch_or_calculate()
self.save()
else:
frappe.msgprint(_("Reached Root"))

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBisectAccountingStatements(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Bisect Nodes", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,97 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2023-09-27 14:56:38.112462",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"root",
"left_child",
"right_child",
"period_from_date",
"period_to_date",
"difference",
"balance_sheet_summary",
"profit_loss_summary",
"generated"
],
"fields": [
{
"fieldname": "root",
"fieldtype": "Link",
"label": "Root",
"options": "Bisect Nodes"
},
{
"fieldname": "left_child",
"fieldtype": "Link",
"label": "Left Child",
"options": "Bisect Nodes"
},
{
"fieldname": "right_child",
"fieldtype": "Link",
"label": "Right Child",
"options": "Bisect Nodes"
},
{
"fieldname": "period_from_date",
"fieldtype": "Datetime",
"label": "Period_from_date"
},
{
"fieldname": "period_to_date",
"fieldtype": "Datetime",
"label": "Period To Date"
},
{
"fieldname": "difference",
"fieldtype": "Float",
"label": "Difference"
},
{
"fieldname": "balance_sheet_summary",
"fieldtype": "Float",
"label": "Balance Sheet Summary"
},
{
"fieldname": "profit_loss_summary",
"fieldtype": "Float",
"label": "Profit and Loss Summary"
},
{
"default": "0",
"fieldname": "generated",
"fieldtype": "Check",
"label": "Generated"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-12-01 17:46:12.437996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bisect Nodes",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,29 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class BisectNodes(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
balance_sheet_summary: DF.Float
difference: DF.Float
generated: DF.Check
left_child: DF.Link | None
name: DF.Int | None
period_from_date: DF.Datetime | None
period_to_date: DF.Datetime | None
profit_loss_summary: DF.Float
right_child: DF.Link | None
root: DF.Link | None
# end: auto-generated types
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBisectNodes(FrappeTestCase):
pass

View File

@ -1,457 +1,152 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 0,
"creation": "2018-06-18 16:51:49.994750", "creation": "2018-06-18 16:51:49.994750",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"naming_series",
"user",
"date",
"from_time",
"time",
"expense",
"custody",
"returns",
"outstanding_amount",
"payments",
"net_amount",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "POS-CLO-", "default": "POS-CLO-",
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Series", "label": "Series",
"length": 0,
"no_copy": 0,
"options": "POS-CLO-", "options": "POS-CLO-",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user", "fieldname": "user",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "User", "label": "User",
"length": 0,
"no_copy": 0,
"options": "User", "options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "reqd": 1
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today", "default": "Today",
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Date", "label": "Date",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_time", "fieldname": "from_time",
"fieldtype": "Time", "fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "From Time", "label": "From Time",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "time", "fieldname": "time",
"fieldtype": "Time", "fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "To Time", "label": "To Time",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "expense", "fieldname": "expense",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0, "label": "Expense"
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expense",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "custody", "fieldname": "custody",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0, "label": "Custody"
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Custody",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "returns", "fieldname": "returns",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Returns", "label": "Returns",
"length": 0, "precision": "2"
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00", "default": "0.00",
"fieldname": "outstanding_amount", "fieldname": "outstanding_amount",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Outstanding Amount", "label": "Outstanding Amount",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.0",
"fieldname": "payments", "fieldname": "payments",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payments", "label": "Payments",
"length": 0, "options": "Cashier Closing Payments"
"no_copy": 0,
"options": "Cashier Closing Payments",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "net_amount", "fieldname": "net_amount",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1, "in_filter": 1,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Net Amount", "label": "Net Amount",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Cashier Closing", "options": "Cashier Closing",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [],
"istable": 0, "modified": "2023-12-28 13:15:46.858427",
"max_attachments": 0,
"modified": "2019-02-19 08:35:24.157327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Cashier Closing", "name": "Cashier Closing",
"name_case": "", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "states": [],
"track_seen": 0, "track_changes": 1
"track_views": 0 }
}

View File

@ -80,7 +80,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"depends_on": "eval: doc.coupon_type == \"Promotional\"", "depends_on": "eval: doc.coupon_type == \"Promotional\"",
@ -115,7 +115,7 @@
"read_only": 1 "read_only": 1
} }
], ],
"modified": "2019-10-19 14:48:14.602481", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Coupon Code", "name": "Coupon Code",

View File

@ -44,7 +44,7 @@ class ExchangeRateRevaluation(Document):
self.set_total_gain_loss() self.set_total_gain_loss()
def validate_rounding_loss_allowance(self): def validate_rounding_loss_allowance(self):
if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1): if self.rounding_loss_allowance < 0 or self.rounding_loss_allowance >= 1:
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1")) frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
def set_total_gain_loss(self): def set_total_gain_loss(self):

View File

@ -82,11 +82,11 @@
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-11-05 12:16:53.081573", "modified": "2024-01-17 13:06:01.608953",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year", "name": "Fiscal Year",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"create": 1, "create": 1,
@ -118,6 +118,14 @@
{ {
"read": 1, "read": 1,
"role": "Employee" "role": "Employee"
},
{
"read": 1,
"role": "Accounts Manager"
},
{
"read": 1,
"role": "Stock Manager"
} }
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,

View File

@ -39,7 +39,7 @@ def test_record_generator():
] ]
start = 2012 start = 2012
end = now_datetime().year + 5 end = now_datetime().year + 25
for year in range(start, end): for year in range(start, end):
test_records.append( test_records.append(
{ {

View File

@ -21,6 +21,7 @@
"against_voucher_type", "against_voucher_type",
"against_voucher", "against_voucher",
"voucher_type", "voucher_type",
"voucher_subtype",
"voucher_no", "voucher_no",
"voucher_detail_no", "voucher_detail_no",
"project", "project",
@ -142,8 +143,7 @@
"label": "Against Voucher Type", "label": "Against Voucher Type",
"oldfieldname": "against_voucher_type", "oldfieldname": "against_voucher_type",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "DocType", "options": "DocType"
"search_index": 1
}, },
{ {
"fieldname": "against_voucher", "fieldname": "against_voucher",
@ -162,8 +162,7 @@
"label": "Voucher Type", "label": "Voucher Type",
"oldfieldname": "voucher_type", "oldfieldname": "voucher_type",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "DocType", "options": "DocType"
"search_index": 1
}, },
{ {
"fieldname": "voucher_no", "fieldname": "voucher_no",
@ -280,13 +279,18 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Credit Amount in Transaction Currency", "label": "Credit Amount in Transaction Currency",
"options": "transaction_currency" "options": "transaction_currency"
},
{
"fieldname": "voucher_subtype",
"fieldtype": "Small Text",
"label": "Voucher Subtype"
} }
], ],
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"links": [], "links": [],
"modified": "2023-08-16 21:38:44.072267", "modified": "2023-09-26 12:03:23.031733",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "GL Entry", "name": "GL Entry",
@ -321,4 +325,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@ -39,6 +39,8 @@ class GLEntry(Document):
account: DF.Link | None account: DF.Link | None
account_currency: DF.Link | None account_currency: DF.Link | None
against: DF.Text | None against: DF.Text | None
against_link: DF.DynamicLink | None
against_type: DF.Link | None
against_voucher: DF.DynamicLink | None against_voucher: DF.DynamicLink | None
against_voucher_type: DF.Link | None against_voucher_type: DF.Link | None
company: DF.Link | None company: DF.Link | None
@ -66,6 +68,7 @@ class GLEntry(Document):
transaction_exchange_rate: DF.Float transaction_exchange_rate: DF.Float
voucher_detail_no: DF.Data | None voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None voucher_no: DF.DynamicLink | None
voucher_subtype: DF.SmallText | None
voucher_type: DF.Link | None voucher_type: DF.Link | None
# end: auto-generated types # end: auto-generated types
@ -432,8 +435,8 @@ def update_outstanding_amt(
def validate_frozen_account(account, adv_adj=None): def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account") frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == "Yes" and not adv_adj: if frozen_account == "Yes" and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( frozen_accounts_modifier = frappe.db.get_single_value(
"Accounts Settings", None, "frozen_accounts_modifier" "Accounts Settings", "frozen_accounts_modifier"
) )
if not frozen_accounts_modifier: if not frozen_accounts_modifier:

View File

@ -154,7 +154,7 @@ frappe.ui.form.on('Invoice Discounting', {
} }
}); });
}, },
primary_action_label: __('Get Invocies') primary_action_label: __('Get Invoices')
}); });
d.show(); d.show();
}, },

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"]; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -150,6 +150,20 @@ class JournalEntry(AccountsController):
if not self.title: if not self.title:
self.title = self.get_title() self.title = self.get_title()
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("submit", timeout=4600)
else:
return self._submit()
def cancel(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
self.queue_action("cancel", timeout=4600)
else:
return self._cancel()
def on_submit(self): def on_submit(self):
self.validate_cheque_info() self.validate_cheque_info()
self.check_credit_limit() self.check_credit_limit()
@ -170,6 +184,8 @@ class JournalEntry(AccountsController):
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger", "Repost Accounting Ledger",
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
) )
self.make_gl_entries(1) self.make_gl_entries(1)
self.update_advance_paid() self.update_advance_paid()
@ -184,9 +200,12 @@ class JournalEntry(AccountsController):
def update_advance_paid(self): def update_advance_paid(self):
advance_paid = frappe._dict() advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
for d in self.get("accounts"): for d in self.get("accounts"):
if d.is_advance: if d.is_advance:
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"): if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name) advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items(): for voucher_type, order_list in advance_paid.items():
@ -629,7 +648,7 @@ class JournalEntry(AccountsController):
) )
# set totals # set totals
if not d.reference_name in self.reference_totals: if d.reference_name not in self.reference_totals:
self.reference_totals[d.reference_name] = 0.0 self.reference_totals[d.reference_name] = 0.0
if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"): if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):

View File

@ -1,12 +1,18 @@
frappe.listview_settings['Journal Entry'] = { frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"], add_fields: [
get_indicator: function(doc) { "voucher_type",
if(doc.docstatus==0) { "posting_date",
return [__("Draft", "red", "docstatus,=,0")] "total_debit",
} else if(doc.docstatus==2) { "company",
return [__("Cancelled", "grey", "docstatus,=,2")] "user_remark",
} else { ],
return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type] get_indicator: function (doc) {
if (doc.docstatus === 1) {
return [
__(doc.voucher_type),
"blue",
`voucher_type,=,${doc.voucher_type}`,
];
} }
} },
}; };

View File

@ -1,97 +1,94 @@
[ [
{ {
"cheque_date": "2013-03-14", "cheque_date": "2013-03-14",
"cheque_no": "33", "cheque_no": "33",
"company": "_Test Company", "company": "_Test Company",
"doctype": "Journal Entry", "doctype": "Journal Entry",
"accounts": [ "accounts": [
{ {
"account": "Debtors - _TC", "account": "Debtors - _TC",
"party_type": "Customer", "party_type": "Customer",
"party": "_Test Customer", "party": "_Test Customer",
"credit_in_account_currency": 400.0, "credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0, "debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account", "doctype": "Journal Entry Account",
"parentfield": "accounts", "parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC" "cost_center": "_Test Cost Center - _TC"
}, },
{ {
"account": "_Test Bank - _TC", "account": "_Test Bank - _TC",
"credit_in_account_currency": 0.0, "credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0, "debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account", "doctype": "Journal Entry Account",
"parentfield": "accounts", "parentfield": "accounts",
"cost_center": "_Test Cost Center - _TC" "cost_center": "_Test Cost Center - _TC"
} }
], ],
"naming_series": "_T-Journal Entry-", "naming_series": "_T-Journal Entry-",
"posting_date": "2013-02-14", "posting_date": "2013-02-14",
"user_remark": "test", "user_remark": "test",
"voucher_type": "Bank Entry" "voucher_type": "Bank Entry"
}, },
{
{ "cheque_date": "2013-02-14",
"cheque_date": "2013-02-14", "cheque_no": "33",
"cheque_no": "33", "company": "_Test Company",
"company": "_Test Company", "doctype": "Journal Entry",
"doctype": "Journal Entry", "accounts": [
"accounts": [ {
{ "account": "_Test Payable - _TC",
"account": "_Test Payable - _TC", "party_type": "Supplier",
"party_type": "Supplier", "party": "_Test Supplier",
"party": "_Test Supplier", "credit_in_account_currency": 0.0,
"credit_in_account_currency": 0.0, "debit_in_account_currency": 400.0,
"debit_in_account_currency": 400.0, "doctype": "Journal Entry Account",
"doctype": "Journal Entry Account", "parentfield": "accounts",
"parentfield": "accounts", "cost_center": "_Test Cost Center - _TC"
"cost_center": "_Test Cost Center - _TC" },
}, {
{ "account": "_Test Bank - _TC",
"account": "_Test Bank - _TC", "credit_in_account_currency": 400.0,
"credit_in_account_currency": 400.0, "debit_in_account_currency": 0.0,
"debit_in_account_currency": 0.0, "doctype": "Journal Entry Account",
"doctype": "Journal Entry Account", "parentfield": "accounts",
"parentfield": "accounts", "cost_center": "_Test Cost Center - _TC"
"cost_center": "_Test Cost Center - _TC" }
} ],
], "naming_series": "_T-Journal Entry-",
"naming_series": "_T-Journal Entry-", "posting_date": "2013-02-14",
"posting_date": "2013-02-14", "user_remark": "test",
"user_remark": "test", "voucher_type": "Bank Entry"
"voucher_type": "Bank Entry" },
},
{
"cheque_date": "2013-02-14",
{ "cheque_no": "33",
"cheque_date": "2013-02-14", "company": "_Test Company",
"cheque_no": "33", "doctype": "Journal Entry",
"company": "_Test Company", "accounts": [
"doctype": "Journal Entry", {
"accounts": [ "account": "Debtors - _TC",
{ "party_type": "Customer",
"account": "Debtors - _TC", "party": "_Test Customer",
"party_type": "Customer", "credit_in_account_currency": 0.0,
"party": "_Test Customer", "debit_in_account_currency": 400.0,
"credit_in_account_currency": 0.0, "doctype": "Journal Entry Account",
"debit_in_account_currency": 400.0, "parentfield": "accounts",
"doctype": "Journal Entry Account", "cost_center": "_Test Cost Center - _TC"
"parentfield": "accounts", },
"cost_center": "_Test Cost Center - _TC" {
}, "account": "Sales - _TC",
{ "credit_in_account_currency": 400.0,
"account": "Sales - _TC", "debit_in_account_currency": 0.0,
"cost_center": "_Test Cost Center - _TC", "doctype": "Journal Entry Account",
"credit_in_account_currency": 400.0, "parentfield": "accounts",
"debit_in_account_currency": 0.0, "cost_center": "_Test Cost Center - _TC"
"doctype": "Journal Entry Account", }
"parentfield": "accounts", ],
"cost_center": "_Test Cost Center - _TC" "naming_series": "_T-Journal Entry-",
} "posting_date": "2013-02-14",
], "user_remark": "test",
"naming_series": "_T-Journal Entry-", "voucher_type": "Bank Entry"
"posting_date": "2013-02-14", }
"user_remark": "test",
"voucher_type": "Bank Entry"
}
] ]

View File

@ -280,14 +280,13 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Reference Detail No", "label": "Reference Detail No",
"no_copy": 1, "no_copy": 1
"search_index": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-23 11:44:25.841187", "modified": "2023-12-03 23:21:22.205409",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@ -140,7 +140,7 @@ class TestLoyaltyProgram(unittest.TestCase):
"Loyalty Point Entry", "Loyalty Point Entry",
{"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer}, {"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
) )
self.assertEqual(True, not (lpe is None)) self.assertEqual(True, lpe is not None)
# cancelling sales invoice # cancelling sales invoice
si.cancel() si.cancel()

View File

@ -270,7 +270,7 @@ def start_import(invoices):
errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>" errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"
), ),
indicator="red", indicator="red",
title=_("Error Occured"), title=_("Error Occurred"),
) )
return names return names

View File

@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
const today = frappe.datetime.get_today(); const today = frappe.datetime.get_today();
const fields = [ let fields = [
{fieldtype:"Section Break", label: __("Posting Date")}, {fieldtype:"Section Break", label: __("Posting Date")},
{fieldtype:"Date", label: __("From Date"), {fieldtype:"Date", label: __("From Date"),
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)}, fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
fieldname:"outstanding_amt_greater_than", default: 0}, fieldname:"outstanding_amt_greater_than", default: 0},
{fieldtype:"Column Break"}, {fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"}, ];
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() { if (frm.dimension_filters) {
return { let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
"filters": {"company": frm.doc.company}
} fields.push({fieldtype:"Section Break"});
frm.dimension_filters.map((elem, idx)=>{
fields.push({
fieldtype: "Link",
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
options: elem.document_type,
fieldname: elem.fieldname || elem.document_type
});
if(idx+1 == column_break_insertion_point) {
fields.push({fieldtype:"Column Break"});
} }
}, });
{fieldtype:"Column Break"}, }
fields = fields.concat([
{fieldtype:"Section Break"}, {fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
]; ]);
let btn_text = ""; let btn_text = "";
@ -747,6 +758,10 @@ frappe.ui.form.on('Payment Entry', {
args["get_orders_to_be_billed"] = true; args["get_orders_to_be_billed"] = true;
} }
if (frm.doc.book_advance_payments_in_separate_party_account) {
args["book_advance_payments_in_separate_party_account"] = true;
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount']; frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({ return frappe.call({
@ -929,7 +944,7 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive" if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges) unallocated_amount = (frm.doc.base_received_amount + total_deductions - flt(frm.doc.base_total_taxes_and_charges)
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay" } else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions

View File

@ -87,12 +87,14 @@
"status", "status",
"custom_remarks", "custom_remarks",
"remarks", "remarks",
"base_in_words",
"column_break_16", "column_break_16",
"letter_head", "letter_head",
"print_heading", "print_heading",
"bank", "bank",
"bank_account_no", "bank_account_no",
"payment_order", "payment_order",
"in_words",
"subscription_section", "subscription_section",
"auto_repeat", "auto_repeat",
"amended_from", "amended_from",
@ -223,6 +225,7 @@
"fieldname": "party_balance", "fieldname": "party_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Party Balance", "label": "Party Balance",
"no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -746,6 +749,20 @@
"hidden": 1, "hidden": 1,
"label": "Book Advance Payments in Separate Party Account", "label": "Book Advance Payments in Separate Party Account",
"read_only": 1 "read_only": 1
},
{
"fieldname": "base_in_words",
"fieldtype": "Small Text",
"label": "In Words (Company Currency)",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "in_words",
"fieldtype": "Small Text",
"label": "In Words",
"print_hide": 1,
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@ -759,7 +776,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2023-11-23 12:07:20.887885", "modified": "2024-01-08 13:17:15.744754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -13,6 +13,7 @@ from pypika import Case
from pypika.functions import Coalesce, Sum from pypika.functions import Coalesce, Sum
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.bank_account.bank_account import ( from erpnext.accounts.doctype.bank_account.bank_account import (
get_bank_account_details, get_bank_account_details,
get_party_bank_account, get_party_bank_account,
@ -50,6 +51,88 @@ class InvalidPaymentEntry(ValidationError):
class PaymentEntry(AccountsController): class PaymentEntry(AccountsController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.advance_taxes_and_charges.advance_taxes_and_charges import (
AdvanceTaxesandCharges,
)
from erpnext.accounts.doctype.payment_entry_deduction.payment_entry_deduction import (
PaymentEntryDeduction,
)
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
PaymentEntryReference,
)
amended_from: DF.Link | None
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
base_paid_amount: DF.Currency
base_paid_amount_after_tax: DF.Currency
base_received_amount: DF.Currency
base_received_amount_after_tax: DF.Currency
base_total_allocated_amount: DF.Currency
base_total_taxes_and_charges: DF.Currency
book_advance_payments_in_separate_party_account: DF.Check
clearance_date: DF.Date | None
company: DF.Link
contact_email: DF.Data | None
contact_person: DF.Link | None
cost_center: DF.Link | None
custom_remarks: DF.Check
deductions: DF.Table[PaymentEntryDeduction]
difference_amount: DF.Currency
letter_head: DF.Link | None
mode_of_payment: DF.Link | None
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
paid_amount: DF.Currency
paid_amount_after_tax: DF.Currency
paid_from: DF.Link
paid_from_account_balance: DF.Currency
paid_from_account_currency: DF.Link
paid_from_account_type: DF.Data | None
paid_to: DF.Link
paid_to_account_balance: DF.Currency
paid_to_account_currency: DF.Link
paid_to_account_type: DF.Data | None
party: DF.DynamicLink | None
party_balance: DF.Currency
party_bank_account: DF.Link | None
party_name: DF.Data | None
party_type: DF.Link | None
payment_order: DF.Link | None
payment_order_status: DF.Literal["Initiated", "Payment Ordered"]
payment_type: DF.Literal["Receive", "Pay", "Internal Transfer"]
posting_date: DF.Date
print_heading: DF.Link | None
project: DF.Link | None
purchase_taxes_and_charges_template: DF.Link | None
received_amount: DF.Currency
received_amount_after_tax: DF.Currency
reference_date: DF.Date | None
reference_no: DF.Data | None
references: DF.Table[PaymentEntryReference]
remarks: DF.SmallText | None
sales_taxes_and_charges_template: DF.Link | None
source_exchange_rate: DF.Float
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
target_exchange_rate: DF.Float
tax_withholding_category: DF.Link | None
taxes: DF.Table[AdvanceTaxesandCharges]
title: DF.Data | None
total_allocated_amount: DF.Currency
total_taxes_and_charges: DF.Currency
unallocated_amount: DF.Currency
# end: auto-generated types
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PaymentEntry, self).__init__(*args, **kwargs) super(PaymentEntry, self).__init__(*args, **kwargs)
if not self.is_new(): if not self.is_new():
@ -95,6 +178,7 @@ class PaymentEntry(AccountsController):
self.validate_paid_invoices() self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked() self.ensure_supplier_is_not_blocked()
self.set_status() self.set_status()
self.set_total_in_words()
def on_submit(self): def on_submit(self):
if self.difference_amount: if self.difference_amount:
@ -107,7 +191,7 @@ class PaymentEntry(AccountsController):
def set_liability_account(self): def set_liability_account(self):
# Auto setting liability account should only be done during 'draft' status # Auto setting liability account should only be done during 'draft' status
if self.docstatus > 0: if self.docstatus > 0 or self.payment_type == "Internal Transfer":
return return
if not frappe.db.get_value( if not frappe.db.get_value(
@ -256,6 +340,7 @@ class PaymentEntry(AccountsController):
"get_outstanding_invoices": True, "get_outstanding_invoices": True,
"get_orders_to_be_billed": True, "get_orders_to_be_billed": True,
"vouchers": vouchers, "vouchers": vouchers,
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
}, },
validate=True, validate=True,
) )
@ -369,12 +454,12 @@ class PaymentEntry(AccountsController):
self.set(self.party_account_field, party_account) self.set(self.party_account_field, party_account)
self.party_account = party_account self.party_account = party_account
if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance): if self.paid_from and not self.paid_from_account_currency and not self.paid_from_account_balance:
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center) acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
self.paid_from_account_currency = acc.account_currency self.paid_from_account_currency = acc.account_currency
self.paid_from_account_balance = acc.account_balance self.paid_from_account_balance = acc.account_balance
if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance): if self.paid_to and not self.paid_to_account_currency and not self.paid_to_account_balance:
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center) acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
self.paid_to_account_currency = acc.account_currency self.paid_to_account_currency = acc.account_currency
self.paid_to_account_balance = acc.account_balance self.paid_to_account_balance = acc.account_balance
@ -390,8 +475,9 @@ class PaymentEntry(AccountsController):
) -> None: ) -> None:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount: if d.allocated_amount:
if update_ref_details_only_for and ( if (
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for update_ref_details_only_for
and (d.reference_doctype, d.reference_name) not in update_ref_details_only_for
): ):
continue continue
@ -701,8 +787,23 @@ class PaymentEntry(AccountsController):
self.db_set("status", self.status, update_modified=True) self.db_set("status", self.status, update_modified=True)
def set_total_in_words(self):
from frappe.utils import money_in_words
if self.payment_type in ("Pay", "Internal Transfer"):
base_amount = abs(self.base_paid_amount)
amount = abs(self.paid_amount)
currency = self.paid_from_account_currency
elif self.payment_type == "Receive":
base_amount = abs(self.base_received_amount)
amount = abs(self.received_amount)
currency = self.paid_to_account_currency
self.base_in_words = money_in_words(base_amount, self.company_currency)
self.in_words = money_in_words(amount, currency)
def set_tax_withholding(self): def set_tax_withholding(self):
if not self.party_type == "Supplier": if self.party_type != "Supplier":
return return
if not self.apply_tax_withholding_amount: if not self.apply_tax_withholding_amount:
@ -793,7 +894,7 @@ class PaymentEntry(AccountsController):
self.base_received_amount = self.base_paid_amount self.base_received_amount = self.base_paid_amount
if ( if (
self.paid_from_account_currency == self.paid_to_account_currency self.paid_from_account_currency == self.paid_to_account_currency
and not self.payment_type == "Internal Transfer" and self.payment_type != "Internal Transfer"
): ):
self.received_amount = self.paid_amount self.received_amount = self.paid_amount
@ -841,7 +942,10 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float: def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0 base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type. # When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes # This is so there are no Exchange Gain/Loss generated for such doctypes
@ -1339,8 +1443,11 @@ class PaymentEntry(AccountsController):
def update_advance_paid(self): def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc( frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid() ).set_total_advance_paid()
@ -1587,6 +1694,13 @@ def get_outstanding_reference_documents(args, validate=False):
condition += " and cost_center='%s'" % args.get("cost_center") condition += " and cost_center='%s'" % args.get("cost_center")
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = { date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"], "posting_date": ["from_posting_date", "to_posting_date"],
"due_date": ["from_due_date", "to_due_date"], "due_date": ["from_due_date", "to_due_date"],
@ -1614,11 +1728,16 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = [] outstanding_invoices = []
negative_outstanding_invoices = [] negative_outstanding_invoices = []
if args.get("book_advance_payments_in_separate_party_account"):
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
else:
party_account = args.get("party_account")
if args.get("get_outstanding_invoices"): if args.get("get_outstanding_invoices"):
outstanding_invoices = get_outstanding_invoices( outstanding_invoices = get_outstanding_invoices(
args.get("party_type"), args.get("party_type"),
args.get("party"), args.get("party"),
get_party_account(args.get("party_type"), args.get("party"), args.get("company")), party_account,
common_filter=common_filter, common_filter=common_filter,
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),
@ -1757,7 +1876,7 @@ def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates:
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date" "Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
) )
for payment_term in payment_schedule: for payment_term in payment_schedule:
if not payment_term.outstanding > 0.1: if payment_term.outstanding <= 0.1:
continue continue
doc_details = exc_rates.get(payment_term.parent, None) doc_details = exc_rates.get(payment_term.parent, None)
@ -1815,6 +1934,12 @@ def get_orders_to_be_billed(
if doc and hasattr(doc, "cost_center") and doc.cost_center: if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center condition = " and cost_center='%s'" % cost_center
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
if party_account_currency == company_currency: if party_account_currency == company_currency:
grand_total_field = "base_grand_total" grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total" rounded_total_field = "base_rounded_total"

View File

@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type(__('Allocate'), null, 'default'); this.frm.change_custom_button_type(__('Allocate'), null, 'default');
} }
this.frm.trigger("set_query_for_dimension_filters");
// check for any running reconciliation jobs // check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) { if (this.frm.doc.receivable_payable_account) {
this.frm.call({ this.frm.call({
@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
} }
set_query_for_dimension_filters() {
frappe.call({
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
args: {
company: this.frm.doc.company,
},
callback: (r) => {
if (!r.exc && r.message) {
r.message.forEach(x => {
this.frm.set_query(x.fieldname, () => {
return {
'filters': x.filters
};
});
});
}
}
});
}
company() { company() {
this.frm.set_value('party', ''); this.frm.set_value('party', '');

View File

@ -25,7 +25,9 @@
"invoice_limit", "invoice_limit",
"payment_limit", "payment_limit",
"bank_cash_account", "bank_cash_account",
"accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break",
"sec_break1", "sec_break1",
"invoice_name", "invoice_name",
"invoices", "invoices",
@ -39,6 +41,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
@ -208,6 +211,18 @@
"fieldname": "payment_name", "fieldname": "payment_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Filter on Payment" "label": "Filter on Payment"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.invoices.length == 0",
"depends_on": "eval:doc.receivable_payable_account",
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions Filter"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
@ -215,7 +230,7 @@
"is_virtual": 1, "is_virtual": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-11-17 17:33:55.701726", "modified": "2024-01-18 11:56:20.234667",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation", "name": "Payment Reconciliation",

View File

@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running, is_any_doc_running,
) )
@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions = [] self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = [] self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = [] self.ple_posting_date_filter = []
self.dimensions = get_dimensions()[0]
def load_from_db(self): def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly. # 'modified' attribute is required for `run_doc_method` to work properly.
@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
if self.payment_name: if self.payment_name:
condition.update({"name": self.payment_name}) condition.update({"name": self.payment_name})
# pass dynamic dimension filter values to query builder
dimensions = {}
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
dimensions.update({dimension: self.get(dimension)})
condition.update({"accounting_dimensions": dimensions})
payment_entries = get_advance_payment_entries_for_regional( payment_entries = get_advance_payment_entries_for_regional(
self.party_type, self.party_type,
self.party, self.party,
@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
return payment_entries return payment_entries
def get_jv_entries(self): def get_jv_entries(self):
condition = self.get_conditions() je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions = self.get_journal_filter_conditions()
# Dimension filters
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
conditions.append(jea[dimension] == self.get(dimension))
if self.payment_name: if self.payment_name:
condition += f" and t1.name like '%%{self.payment_name}%%'" conditions.append(je.name.like(f"%%{self.payment_name}%%"))
if self.get("cost_center"): if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' " conditions.append(jea.cost_center == self.cost_center)
dr_or_cr = ( dr_or_cr = (
"credit_in_account_currency" "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable" if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency" else "debit_in_account_currency"
) )
conditions.append(jea[dr_or_cr].gt(0))
bank_account_condition = ( if self.bank_cash_account:
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
journal_query = (
qb.from_(je)
.inner_join(jea)
.on(jea.parent == je.name)
.select(
ConstantColumn("Journal Entry").as_("reference_type"),
je.name.as_("reference_name"),
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
jea[dr_or_cr].as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
jea.cost_center.as_("cost_center"),
)
.where(
(je.docstatus == 1)
& (jea.party_type == self.party_type)
& (jea.party == self.party)
& (jea.account == self.receivable_payable_account)
& (
(jea.reference_type == "")
| (jea.reference_type.isnull())
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
)
)
.where(Criterion.all(conditions))
.orderby(je.posting_date)
) )
limit = f"limit {self.payment_limit}" if self.payment_limit else " " if self.payment_limit:
journal_query = journal_query.limit(self.payment_limit)
# nosemgrep journal_entries = journal_query.run(as_dict=True)
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency, t2.cost_center as cost_center
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
and t2.party_type = %(party_type)s and t2.party = %(party)s
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
and (t2.reference_type is null or t2.reference_type = '' or
(t2.reference_type in ('Sales Order', 'Purchase Order')
and t2.reference_name is not null and t2.reference_name != ''))
and (CASE
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
THEN 1=1
ELSE {bank_account_condition}
END)
order by t1.posting_date
{limit}
""".format(
**{
"dr_or_cr": dr_or_cr,
"bank_account_condition": bank_account_condition,
"condition": condition,
"limit": limit,
}
),
{
"party_type": self.party_type,
"party": self.party,
"account": self.receivable_payable_account,
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
},
as_dict=1,
)
return list(journal_entries) return list(journal_entries)
@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True, get_payments=True,
accounting_dimensions=self.accounting_dimension_filter_conditions,
) )
for inv in return_outstanding: for inv in return_outstanding:
@ -447,8 +459,15 @@ class PaymentReconciliation(Document):
row = self.append("allocation", {}) row = self.append("allocation", {})
row.update(entry) row.update(entry)
def update_dimension_values_in_allocated_entries(self, res):
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
res[dimension] = self.get(dimension)
return res
def get_allocated_entry(self, pay, inv, allocated_amount): def get_allocated_entry(self, pay, inv, allocated_amount):
return frappe._dict( res = frappe._dict(
{ {
"reference_type": pay.get("reference_type"), "reference_type": pay.get("reference_type"),
"reference_name": pay.get("reference_name"), "reference_name": pay.get("reference_name"),
@ -464,6 +483,9 @@ class PaymentReconciliation(Document):
} }
) )
res = self.update_dimension_values_in_allocated_entries(res)
return res
def reconcile_allocations(self, skip_ref_details_update_for_pe=False): def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
adjust_allocations_for_taxes(self) adjust_allocations_for_taxes(self)
dr_or_cr = ( dr_or_cr = (
@ -486,10 +508,10 @@ class PaymentReconciliation(Document):
reconciled_entry.append(payment_details) reconciled_entry.append(payment_details)
if entry_list: if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe) reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
if dr_or_cr_notes: if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company) reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
@frappe.whitelist() @frappe.whitelist()
def reconcile(self): def reconcile(self):
@ -518,7 +540,7 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries() self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr): def get_payment_details(self, row, dr_or_cr):
return frappe._dict( payment_details = frappe._dict(
{ {
"voucher_type": row.get("reference_type"), "voucher_type": row.get("reference_type"),
"voucher_no": row.get("reference_name"), "voucher_no": row.get("reference_name"),
@ -541,6 +563,12 @@ class PaymentReconciliation(Document):
} }
) )
for x in self.dimensions:
if row.get(x.fieldname):
payment_details[x.fieldname] = row.get(x.fieldname)
return payment_details
def check_mandatory_to_fetch(self): def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname): if not self.get(fieldname):
@ -594,6 +622,27 @@ class PaymentReconciliation(Document):
invoice_exchange_map.update(purchase_invoice_map) invoice_exchange_map.update(purchase_invoice_map)
journals = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
]
journals.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
)
if journals:
journals = list(set(journals))
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
fields=[
"parent as `name`",
"exchange_rate",
],
as_list=1,
)
)
invoice_exchange_map.update(journals_map)
return invoice_exchange_map return invoice_exchange_map
def validate_allocation(self): def validate_allocation(self):
@ -627,6 +676,13 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile: if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table")) frappe.throw(_("No records found in Allocation table"))
def build_dimensions_filter_conditions(self):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear() self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear() self.accounting_dimension_filter_conditions.clear()
@ -650,40 +706,30 @@ class PaymentReconciliation(Document):
if self.to_payment_date: if self.to_payment_date:
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
def get_conditions(self, get_payments=False): self.build_dimensions_filter_conditions()
condition = " and company = '{0}' ".format(self.company)
if self.get("cost_center") and get_payments: def get_journal_filter_conditions(self):
condition = " and cost_center = '{0}' ".format(self.cost_center) conditions = []
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
conditions.append(je.company == self.company)
condition += ( if self.from_payment_date:
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) conditions.append(je.posting_date.gte(self.from_payment_date))
if self.from_payment_date
else "" if self.to_payment_date:
) conditions.append(je.posting_date.lte(self.to_payment_date))
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.minimum_payment_amount: if self.minimum_payment_amount:
condition += ( conditions.append(je.total_debit.gte(self.minimum_payment_amount))
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount: if self.maximum_payment_amount:
condition += ( conditions.append(je.total_debit.lte(self.maximum_payment_amount))
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
return condition return conditions
def reconcile_dr_cr_note(dr_cr_notes, company): def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes: for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@ -733,6 +779,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
} }
) )
# Credit Note(JE) will inherit the same dimension values as payment
dimensions_dict = frappe._dict()
if active_dimensions:
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
jv.accounts[0].update(dimensions_dict)
jv.accounts[1].update(dimensions_dict)
jv.flags.ignore_mandatory = True jv.flags.ignore_mandatory = True
jv.flags.ignore_exchange_rate = True jv.flags.ignore_exchange_rate = True
jv.remark = None jv.remark = None
@ -766,9 +821,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
inv.against_voucher, inv.against_voucher,
None, None,
inv.cost_center, inv.cost_center,
dimensions_dict,
) )
@erpnext.allow_regional @erpnext.allow_regional
def adjust_allocations_for_taxes(doc): def adjust_allocations_for_taxes(doc):
pass pass
@frappe.whitelist()
def get_queries_for_dimension_filters(company: str = None):
dimensions_with_filters = []
for d in get_dimensions()[0]:
filters = {}
meta = frappe.get_meta(d.document_type)
if meta.has_field("company") and company:
filters.update({"company": company})
if meta.is_tree:
filters.update({"is_group": 0})
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
return dimensions_with_filters

View File

@ -24,7 +24,9 @@
"difference_account", "difference_account",
"exchange_rate", "exchange_rate",
"currency", "currency",
"cost_center" "accounting_dimensions_section",
"cost_center",
"dimension_col_break"
], ],
"fields": [ "fields": [
{ {
@ -157,12 +159,21 @@
"fieldname": "gain_loss_posting_date", "fieldname": "gain_loss_posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Difference Posting Date" "label": "Difference Posting Date"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"is_virtual": 1, "is_virtual": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-17 17:33:38.612615", "modified": "2023-12-14 13:38:26.104150",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",

View File

@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
unreconciled_amount: DF.Currency unreconciled_amount: DF.Currency
# end: auto-generated types # end: auto-generated types
pass @staticmethod
def get_list(args):
pass

View File

@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
parenttype: DF.Data parenttype: DF.Data
# end: auto-generated types # end: auto-generated types
pass @staticmethod
def get_list(args):
pass

View File

@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
remark: DF.SmallText | None remark: DF.SmallText | None
# end: auto-generated types # end: auto-generated types
pass @staticmethod
def get_list(args):
pass

View File

@ -169,6 +169,13 @@ class PaymentRequest(Document):
elif self.payment_channel == "Phone": elif self.payment_channel == "Phone":
self.request_phone_payment() self.request_phone_payment()
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def request_phone_payment(self): def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway) controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount() request_amount = self.get_request_amount()
@ -207,6 +214,14 @@ class PaymentRequest(Document):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
self.set_as_cancelled() self.set_as_cancelled()
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks(
"advance_payment_receivable_doctypes"
) + frappe.get_hooks("advance_payment_payable_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
def make_invoice(self): def make_invoice(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart": if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
@ -424,6 +439,15 @@ def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
args = frappe._dict(args) args = frappe._dict(args)
if args.dt not in [
"Sales Order",
"Purchase Order",
"Sales Invoice",
"Purchase Invoice",
"POS Invoice",
"Fees",
]:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
ref_doc = frappe.get_doc(args.dt, args.dn) ref_doc = frappe.get_doc(args.dt, args.dn)
gateway_account = get_gateway_details(args) or frappe._dict() gateway_account = get_gateway_details(args) or frappe._dict()

View File

@ -11,7 +11,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
SalesInvoice, SalesInvoice,
get_bank_cash_account,
get_mode_of_payment_info, get_mode_of_payment_info,
update_multi_mode_option, update_multi_mode_option,
) )
@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items_qty() self.validate_return_items_qty()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
self.validate_payment_amount() self.validate_payment_amount()
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
@ -371,7 +369,7 @@ class POSInvoice(SalesInvoice):
if d.get("qty") > 0: if d.get("qty") > 0:
frappe.throw( frappe.throw(
_( _(
"Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return." "Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return."
).format(d.idx, frappe.bold(d.item_code)), ).format(d.idx, frappe.bold(d.item_code)),
title=_("Invalid Item"), title=_("Invalid Item"),
) )
@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
update_multi_mode_option(self, pos_profile) update_multi_mode_option(self, pos_profile)
self.paid_amount = 0 self.paid_amount = 0
def set_account_for_mode_of_payment(self):
for pay in self.payments:
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@frappe.whitelist() @frappe.whitelist()
def create_payment_request(self): def create_payment_request(self):
for pay in self.payments: for pay in self.payments:
@ -765,7 +758,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = ( reserved_qty = (
frappe.qb.from_(p_inv) frappe.qb.from_(p_inv)
.from_(p_item) .from_(p_item)
.select(Sum(p_item.qty).as_("qty")) .select(Sum(p_item.stock_qty).as_("stock_qty"))
.where( .where(
(p_inv.name == p_item.parent) (p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "") & (IfNull(p_inv.consolidated_invoice, "") == "")
@ -775,7 +768,7 @@ def get_pos_reserved_qty(item_code, warehouse):
) )
).run(as_dict=True) ).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0 return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist() @frappe.whitelist()
@ -793,7 +786,7 @@ def make_merge_log(invoices):
invoices = json.loads(invoices) invoices = json.loads(invoices)
if len(invoices) == 0: if len(invoices) == 0:
frappe.throw(_("Atleast one invoice has to be selected.")) frappe.throw(_("At least one invoice has to be selected."))
merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(nowdate())

View File

@ -132,7 +132,7 @@ class POSProfile(Document):
if len(customer_groups) != len(set(customer_groups)): if len(customer_groups) != len(set(customer_groups)):
frappe.throw( frappe.throw(
_("Duplicate customer group found in the cutomer group table"), _("Duplicate customer group found in the customer group table"),
title=_("Duplicate Customer Group"), title=_("Duplicate Customer Group"),
) )

View File

@ -339,7 +339,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "col_break1", "fieldname": "col_break1",
@ -608,7 +608,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2023-02-14 04:53:34.887358", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@ -193,7 +193,7 @@ class PricingRule(Document):
def validate_applicable_for_selling_or_buying(self): def validate_applicable_for_selling_or_buying(self):
if not self.selling and not self.buying: if not self.selling and not self.buying:
throw(_("Atleast one of the Selling or Buying must be selected")) throw(_("At least one of the Selling or Buying must be selected"))
if not self.selling and self.applicable_for in [ if not self.selling and self.applicable_for in [
"Customer", "Customer",
@ -286,7 +286,7 @@ class PricingRule(Document):
def validate_price_list_with_currency(self): def validate_price_list_with_currency(self):
if self.currency and self.for_price_list: if self.currency and self.for_price_list:
price_list_currency = frappe.db.get_value("Price List", self.for_price_list, "currency", True) price_list_currency = frappe.db.get_value("Price List", self.for_price_list, "currency", True)
if not self.currency == price_list_currency: if self.currency != price_list_currency:
throw(_("Currency should be same as Price List Currency: {0}").format(price_list_currency)) throw(_("Currency should be same as Price List Currency: {0}").format(price_list_currency))
def validate_dates(self): def validate_dates(self):
@ -579,12 +579,17 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0) item_details[field] += pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)
@frappe.whitelist()
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules, get_applied_pricing_rules,
get_pricing_rule_items, get_pricing_rule_items,
) )
if isinstance(item_details, str):
item_details = json.loads(item_details)
item_details = frappe._dict(item_details)
for d in get_applied_pricing_rules(pricing_rules): for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d): if not d or not frappe.db.exists("Pricing Rule", d):
continue continue

View File

@ -527,7 +527,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
values.extend(warehouses) values.extend(warehouses)
if items: if items:
condition = " and `tab{child_doc}`.{apply_on} in ({items})".format( condition += " and `tab{child_doc}`.{apply_on} in ({items})".format(
child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items)) child_doc=child_doctype, apply_on=apply_on, items=",".join(["%s"] * len(items))
) )
@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc):
if d.price_or_product_discount == "Price": if d.price_or_product_discount == "Price":
if d.apply_discount_on: if d.apply_discount_on:
doc.set("apply_discount_on", d.apply_discount_on) doc.set("apply_discount_on", d.apply_discount_on)
# Variable to track whether the condition has been met
condition_met = False
for field in ["additional_discount_percentage", "discount_amount"]: for field in ["additional_discount_percentage", "discount_amount"]:
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc):
if coupon_code_pricing_rule == d.name: if coupon_code_pricing_rule == d.name:
# if selected coupon code is linked with pricing rule # if selected coupon code is linked with pricing rule
doc.set(field, d.get(pr_field)) doc.set(field, d.get(pr_field))
# Set the condition_met variable to True and break out of the loop
condition_met = True
break
else: else:
# reset discount if not linked # reset discount if not linked
doc.set(field, 0) doc.set(field, 0)
@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
doc.set(field, 0) doc.set(field, 0)
doc.calculate_taxes_and_totals() doc.calculate_taxes_and_totals()
# Break out of the main loop if the condition is met
if condition_met:
break
elif d.price_or_product_discount == "Product": elif d.price_or_product_discount == "Product":
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []}) item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
get_product_discount_rule(d, item_details, doc=doc) get_product_discount_rule(d, item_details, doc=doc)

View File

@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None:
# Update the parent doc about the exception # Update the parent doc about the exception
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback(with_context=True)
if traceback: if traceback:
message = "Traceback: <br>" + traceback message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message) frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
@ -475,7 +475,7 @@ def reconcile(doc: None | str = None) -> None:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
else: else:
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"): if frappe.db.get_value("Process Payment Reconciliation", doc, "status") != "Paused":
# trigger next batch in job # trigger next batch in job
# generate reconcile job name # generate reconcile job name
allocation = get_next_allocation(log) allocation = get_next_allocation(log)

View File

@ -15,6 +15,7 @@
"group_by", "group_by",
"cost_center", "cost_center",
"territory", "territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14", "column_break_14",
"to_date", "to_date",
"finance_book", "finance_book",
@ -376,10 +377,16 @@
"fieldname": "pdf_name", "fieldname": "pdf_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "PDF Name" "label": "PDF Name"
},
{
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
} }
], ],
"links": [], "links": [],
"modified": "2023-08-28 12:59:53.071334", "modified": "2023-12-18 12:20:08.965120",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts", "name": "Process Statement Of Accounts",

View File

@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"] frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"] group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check include_ageing: DF.Check
include_break: DF.Check include_break: DF.Check
letter_head: DF.Link | None letter_head: DF.Link | None
@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {} statement_dict = {}
ageing = "" ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers: for entry in doc.customers:
if doc.include_ageing: if doc.include_ageing:
ageing = set_ageing(doc, entry) ageing = set_ageing(doc, entry)
@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
) )
filters = get_common_filters(doc) filters = get_common_filters(doc)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger": if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))

View File

@ -232,7 +232,7 @@
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto" "label": "Valid Up To"
}, },
{ {
"fieldname": "column_break_26", "fieldname": "column_break_26",
@ -278,7 +278,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-05-06 16:20:22.039078", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme", "name": "Promotional Scheme",

View File

@ -35,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload(); super.onload();
// Ignore linked advances // Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"]; this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle", "Bank Transaction"];
if(!this.frm.doc.__islocal) { if(!this.frm.doc.__islocal) {
// show credit_to in print format // show credit_to in print format
@ -163,6 +163,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
} }
}) })
}, __("Get Items From")); }, __("Get Items From"));
if (!this.frm.doc.is_return) {
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
if (value) {
this.frm.doc.items.forEach((item) => {
this.frm.fields_dict.items.grid.update_docfield_property(
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
);
});
}
});
}
} }
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
@ -396,6 +408,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
} }
on_submit() { on_submit() {
super.on_submit();
$.each(this.frm.doc["items"] || [], function(i, row) { $.each(this.frm.doc["items"] || [], function(i, row) {
if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt) if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt)
}) })

View File

@ -1253,6 +1253,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1 "print_hide": 1
}, },
@ -1612,7 +1613,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-11-29 15:35:44.697496", "modified": "2024-01-26 10:46:00.469053",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
self.set_percentage_received()
def set_percentage_received(self):
total_billed_qty = 0.0
total_received_qty = 0.0
for row in self.items:
if row.purchase_receipt and row.pr_detail and row.received_qty:
total_billed_qty += row.qty
total_received_qty += row.received_qty
if total_billed_qty and total_received_qty:
self.per_received = total_received_qty / total_billed_qty * 100
def validate_release_date(self): def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date): if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -371,7 +383,7 @@ class PurchaseInvoice(BuyingController):
check_list = [] check_list = []
for d in self.get("items"): for d in self.get("items"):
if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt: if d.purchase_order and d.purchase_order not in check_list and not d.purchase_receipt:
check_list.append(d.purchase_order) check_list.append(d.purchase_order)
check_on_hold_or_closed_status("Purchase Order", d.purchase_order) check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
@ -552,7 +564,7 @@ class PurchaseInvoice(BuyingController):
self.against_expense_account = ",".join(against_accounts) self.against_expense_account = ",".join(against_accounts)
def po_required(self): def po_required(self):
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
if frappe.get_value( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
@ -572,7 +584,7 @@ class PurchaseInvoice(BuyingController):
def pr_required(self): def pr_required(self):
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
if frappe.get_value( if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
@ -1084,17 +1096,6 @@ class PurchaseInvoice(BuyingController):
item=item, item=item,
) )
) )
# update gross amount of asset bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
)
if ( if (
self.auto_accounting_for_stock self.auto_accounting_for_stock
and self.is_opening == "No" and self.is_opening == "No"
@ -1119,7 +1120,7 @@ class PurchaseInvoice(BuyingController):
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
"account": stock_rbnb, "account": self.stock_received_but_not_billed,
"against": self.supplier, "against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"), "remarks": self.remarks or _("Accounting Entry for Stock"),
@ -1134,12 +1135,25 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
if item.is_fixed_asset and item.landed_cost_voucher_amount:
self.update_gross_purchase_amount_for_linked_assets(item)
def update_gross_purchase_amount_for_linked_assets(self, item):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} "Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"],
) )
for asset in assets: for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) purchase_amount = flt(item.valuation_rate) * asset.asset_quantity
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) frappe.db.set_value(
"Asset",
asset.name,
{
"gross_purchase_amount": purchase_amount,
"purchase_receipt_amount": purchase_amount,
},
)
def make_stock_adjustment_entry( def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency self, gl_entries, item, voucher_wise_stock_value, account_currency
@ -1449,6 +1463,8 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items", "Repost Payment Ledger Items",
"Repost Accounting Ledger", "Repost Accounting Ledger",
"Repost Accounting Ledger Items", "Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Payment Ledger Entry", "Payment Ledger Entry",
"Tax Withheld Vouchers", "Tax Withheld Vouchers",
"Serial and Batch Bundle", "Serial and Batch Bundle",
@ -1814,10 +1830,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist() @frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None): def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):

View File

@ -14,7 +14,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
@ -51,6 +51,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def test_purchase_invoice_qty(self):
pi = make_purchase_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
pi.save()
# No error with qty=1
pi.items[0].qty = 1
pi.save()
self.assertEqual(pi.items[0].qty, 1)
def test_purchase_invoice_received_qty(self): def test_purchase_invoice_received_qty(self):
""" """
1. Test if received qty is validated against accepted + rejected 1. Test if received qty is validated against accepted + rejected
@ -1227,11 +1237,11 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self): def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value( unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" "Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
) )
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value( frappe.db.set_value(
@ -1422,7 +1432,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pay.cancel() pay.cancel()
frappe.db.set_single_value( frappe.db.set_single_value(
"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
) )
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@ -1985,6 +1995,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def test_debit_note_without_item(self):
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
pi.items[0].item_code = ""
pi.save()
self.assertFalse(pi.items[0].item_code)
pi.submit()
return_pi = make_purchase_invoice(
item_name="_Test Item",
is_return=1,
return_against=pi.name,
qty=-10,
do_not_save=True,
)
return_pi.items[0].item_code = ""
return_pi.save()
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
def set_advance_flag(company, flag, default_account): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(
@ -2094,7 +2124,7 @@ def make_purchase_invoice(**args):
bundle_id = None bundle_id = None
if args.get("batch_no") or args.get("serial_no"): if args.get("batch_no") or args.get("serial_no"):
batches = {} batches = {}
qty = args.qty or 5 qty = args.qty if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -2121,8 +2151,9 @@ def make_purchase_invoice(**args):
"items", "items",
{ {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5, "qty": args.qty if args.qty is not None else 5,
"received_qty": args.received_qty or 0, "received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0, "rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50, "rate": args.rate or 50,

View File

@ -64,6 +64,7 @@
"warehouse", "warehouse",
"from_warehouse", "from_warehouse",
"quality_inspection", "quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"serial_no", "serial_no",
"col_br_wh", "col_br_wh",
@ -288,7 +289,6 @@
"oldfieldname": "import_rate", "oldfieldname": "import_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
"reqd": 1 "reqd": 1
}, },
{ {
@ -914,12 +914,18 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "WIP Composite Asset", "label": "WIP Composite Asset",
"options": "Asset" "options": "Asset"
},
{
"depends_on": "eval:parent.update_stock === 1",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-30 16:26:05.629780", "modified": "2024-01-21 19:46:25.537861",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -126,7 +126,7 @@
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate", "label": "Tax Rate",
"oldfieldname": "rate", "oldfieldname": "rate",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@ -230,7 +230,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-05 20:04:36.618240", "modified": "2024-01-14 10:04:36.618240",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Taxes and Charges", "name": "Purchase Taxes and Charges",
@ -239,4 +239,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -126,7 +126,7 @@ class RepostAccountingLedger(Document):
return rendered_page return rendered_page
def on_submit(self): def on_submit(self):
if len(self.vouchers) > 1: if len(self.vouchers) > 5:
job_name = "repost_accounting_ledger_" + self.name job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue( frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
@ -170,8 +170,6 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries(1) doc.make_gl_entries(1)
doc.make_gl_entries() doc.make_gl_entries()
frappe.db.commit()
def get_allowed_types_from_settings(): def get_allowed_types_from_settings():
return [ return [

View File

@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
self.create_item() self.create_item()
self.update_repost_settings() update_repost_settings()
def teadDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def update_repost_settings(self):
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
def test_01_basic_functions(self): def test_01_basic_functions(self):
si = create_sales_invoice( si = create_sales_invoice(
item=self.item, item=self.item,
@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
# Submit repost document # Submit repost document
ral.save().submit() ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = ( res = (
qb.from_(gl) qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
pe = get_payment_entry(si.doctype, si.name) pe = get_payment_entry(si.doctype, si.name)
pe.save().submit() pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# assert preview data is generated
preview = ral.generate_preview()
self.assertIsNotNone(preview)
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
# with deletion flag set # with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger") ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company ral.company = self.company
@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit() ral.save().submit()
start_repost(ral.name)
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_05_without_deletion_flag(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def update_repost_settings():
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()

View File

@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
except Exception as e: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback(with_context=True)
if traceback: if traceback:
message = "Traceback: <br>" + traceback message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message) frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)

View File

@ -37,7 +37,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload(); super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle", "Bank Transaction",
];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
@ -197,6 +198,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
on_submit(doc, dt, dn) { on_submit(doc, dt, dn) {
var me = this; var me = this;
super.on_submit();
if (frappe.get_route()[0] != 'Form') { if (frappe.get_route()[0] != 'Form') {
return return
} }
@ -895,8 +897,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0); frm.events.append_time_log(frm, timesheet, 1.0);
} }
}); });
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals"); frm.trigger("calculate_timesheet_totals");
frm.refresh();
}, },
async get_exchange_rate(frm, from_currency, to_currency) { async get_exchange_rate(frm, from_currency, to_currency) {

View File

@ -138,6 +138,7 @@
"loyalty_amount", "loyalty_amount",
"column_break_77", "column_break_77",
"loyalty_program", "loyalty_program",
"dont_create_loyalty_points",
"loyalty_redemption_account", "loyalty_redemption_account",
"loyalty_redemption_cost_center", "loyalty_redemption_cost_center",
"contact_and_address_tab", "contact_and_address_tab",
@ -1041,8 +1042,7 @@
"label": "Loyalty Program", "label": "Loyalty Program",
"no_copy": 1, "no_copy": 1,
"options": "Loyalty Program", "options": "Loyalty Program",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -2162,6 +2162,14 @@
"fieldname": "update_billed_amount_in_delivery_note", "fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note" "label": "Update Billed Amount in Delivery Note"
},
{
"default": "0",
"depends_on": "loyalty_program",
"fieldname": "dont_create_loyalty_points",
"fieldtype": "Check",
"label": "Don't Create Loyalty Points",
"no_copy": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2174,7 +2182,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-11-23 16:56:29.679499", "modified": "2024-01-02 17:25:46.027523",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -117,6 +117,7 @@ class SalesInvoice(SellingController):
discount_amount: DF.Currency discount_amount: DF.Currency
dispatch_address: DF.SmallText | None dispatch_address: DF.SmallText | None
dispatch_address_name: DF.Link | None dispatch_address_name: DF.Link | None
dont_create_loyalty_points: DF.Check
due_date: DF.Date | None due_date: DF.Date | None
from_date: DF.Date | None from_date: DF.Date | None
grand_total: DF.Currency grand_total: DF.Currency
@ -420,7 +421,8 @@ class SalesInvoice(SellingController):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
def before_save(self): def before_save(self):
set_account_for_mode_of_payment(self) self.set_account_for_mode_of_payment()
self.set_paid_amount()
def on_submit(self): def on_submit(self):
self.validate_pos_paid_amount() self.validate_pos_paid_amount()
@ -458,7 +460,7 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.check_credit_limit() self.check_credit_limit()
if not cint(self.is_pos) == 1 and not self.is_return: if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv() self.update_against_document_in_jv()
self.update_time_sheet(self.name) self.update_time_sheet(self.name)
@ -471,7 +473,12 @@ class SalesInvoice(SellingController):
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program # create the loyalty point ledger entry if the customer is enrolled in any loyalty program
if not self.is_return and not self.is_consolidated and self.loyalty_program: if (
not self.is_return
and not self.is_consolidated
and self.loyalty_program
and not self.dont_create_loyalty_points
):
self.make_loyalty_point_entry() self.make_loyalty_point_entry()
elif ( elif (
self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program
@ -586,6 +593,8 @@ class SalesInvoice(SellingController):
"Serial and Batch Bundle", "Serial and Batch Bundle",
) )
self.delete_auto_created_batches()
def update_status_updater_args(self): def update_status_updater_args(self):
if cint(self.update_stock): if cint(self.update_stock):
self.status_updater.append( self.status_updater.append(
@ -704,9 +713,6 @@ class SalesInvoice(SellingController):
): ):
data.sales_invoice = sales_invoice data.sales_invoice = sales_invoice
def on_update(self):
self.set_paid_amount()
def on_update_after_submit(self): def on_update_after_submit(self):
if hasattr(self, "repost_required"): if hasattr(self, "repost_required"):
fields_to_check = [ fields_to_check = [
@ -737,6 +743,11 @@ class SalesInvoice(SellingController):
self.paid_amount = paid_amount self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount self.base_paid_amount = base_paid_amount
def set_account_for_mode_of_payment(self):
for payment in self.payments:
if not payment.account:
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self): def validate_time_sheets_are_submitted(self):
for data in self.timesheets: for data in self.timesheets:
if data.time_sheet: if data.time_sheet:
@ -1960,9 +1971,9 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
if inter_company_reference: if inter_company_reference:
doc = frappe.get_doc(ref_doc, inter_company_reference) doc = frappe.get_doc(ref_doc, inter_company_reference)
ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer
if not frappe.db.get_value(partytype, {"represents_company": doc.company}, "name") == party: if frappe.db.get_value(partytype, {"represents_company": doc.company}, "name") != party:
frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(_(partytype))) frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(_(partytype)))
if not frappe.get_cached_value(ref_partytype, ref_party, "represents_company") == company: if frappe.get_cached_value(ref_partytype, ref_party, "represents_company") != company:
frappe.throw(_("Invalid Company for Inter Company Transaction.")) frappe.throw(_("Invalid Company for Inter Company Transaction."))
elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party: elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party:
@ -1972,7 +1983,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc
filters={"parenttype": partytype, "parent": party}, filters={"parenttype": partytype, "parent": party},
) )
companies = [d.company for d in companies] companies = [d.company for d in companies]
if not company in companies: if company not in companies:
frappe.throw( frappe.throw(
_("{0} not allowed to transact with {1}. Please change the Company.").format( _("{0} not allowed to transact with {1}. Please change the Company.").format(
_(partytype), company _(partytype), company
@ -2105,12 +2116,6 @@ def make_sales_return(source_name, target_doc=None):
return make_return_doc("Sales Invoice", source_name, target_doc) return make_return_doc("Sales Invoice", source_name, target_doc)
def set_account_for_mode_of_payment(self):
for data in self.payments:
if not data.account:
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
def get_inter_company_details(doc, doctype): def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
parties = frappe.db.get_all( parties = frappe.db.get_all(
@ -2356,9 +2361,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def get_received_items(reference_name, doctype, reference_fieldname): def get_received_items(reference_name, doctype, reference_fieldname):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
filters = {
reference_field: reference_name,
"docstatus": 1,
}
target_doctypes = frappe.get_all( target_doctypes = frappe.get_all(
doctype, doctype,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, filters=filters,
as_list=True, as_list=True,
) )
@ -2540,10 +2554,6 @@ def get_loyalty_programs(customer):
return lp_details return lp_details
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
@frappe.whitelist() @frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None): def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name) invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@ -23,7 +23,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule, get_depr_schedule,
) )
from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@ -72,6 +72,16 @@ class TestSalesInvoice(FrappeTestCase):
def tearDownClass(self): def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
def test_sales_invoice_qty(self):
si = create_sales_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
si.save()
# No error with qty=1
si.items[0].qty = 1
si.save()
self.assertEqual(si.items[0].qty, 1)
def test_timestamp_change(self): def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0]) w = frappe.copy_doc(test_records[0])
w.docstatus = 0 w.docstatus = 0
@ -1414,10 +1424,11 @@ class TestSalesInvoice(FrappeTestCase):
def test_serialized_cancel(self): def test_serialized_cancel(self):
si = self.test_serialized() si = self.test_serialized()
si.cancel() si.reload()
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle) serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
si.cancel()
self.assertEqual( self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
) )
@ -2793,6 +2804,12 @@ class TestSalesInvoice(FrappeTestCase):
@change_settings("Selling Settings", {"enable_discount_accounting": 1}) @change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self): def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
additional_discount_account = create_account( additional_discount_account = create_account(
account_name="Discount Account", account_name="Discount Account",
parent_account="Indirect Expenses - _TC", parent_account="Indirect Expenses - _TC",
@ -3629,7 +3646,7 @@ def create_sales_invoice(**args):
bundle_id = None bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")): if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {} batches = {}
qty = args.qty or 1 qty = args.qty if args.qty is not None else 1
item_code = args.item or args.item_code or "_Test Item" item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"): if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty}) batches = frappe._dict({args.batch_no: qty})
@ -3661,7 +3678,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item", "description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse, "target_warehouse": args.target_warehouse,
"qty": args.qty or 1, "qty": args.qty if args.qty is not None else 1,
"uom": args.uom or "Nos", "uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos", "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,

View File

@ -81,6 +81,7 @@
"warehouse", "warehouse",
"target_warehouse", "target_warehouse",
"quality_inspection", "quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"batch_no", "batch_no",
"incoming_rate", "incoming_rate",
@ -897,12 +898,18 @@
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1, "print_hide": 1,
"search_index": 1 "search_index": 1
},
{
"depends_on": "eval:parent.update_stock === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-14 18:34:10.479329", "modified": "2023-12-29 13:03:14.121298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -108,7 +108,7 @@
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate", "label": "Tax Rate",
"oldfieldname": "rate", "oldfieldname": "rate",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@ -218,7 +218,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-10-17 13:08:17.776528", "modified": "2022-10-18 13:08:17.776528",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",
@ -227,4 +227,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": []
} }

View File

@ -142,12 +142,12 @@ class ShippingRule(Document):
} }
if self.shipping_rule_type == "Selling": if self.shipping_rule_type == "Selling":
# check if not applied on purchase # check if not applied on purchase
if not doc.meta.get_field("taxes").options == "Sales Taxes and Charges": if doc.meta.get_field("taxes").options != "Sales Taxes and Charges":
frappe.throw(_("Shipping rule only applicable for Selling")) frappe.throw(_("Shipping rule only applicable for Selling"))
shipping_charge["doctype"] = "Sales Taxes and Charges" shipping_charge["doctype"] = "Sales Taxes and Charges"
else: else:
# check if not applied on sales # check if not applied on sales
if not doc.meta.get_field("taxes").options == "Purchase Taxes and Charges": if doc.meta.get_field("taxes").options != "Purchase Taxes and Charges":
frappe.throw(_("Shipping rule only applicable for Buying")) frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges" shipping_charge["doctype"] = "Purchase Taxes and Charges"

View File

@ -51,7 +51,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", "options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1 "read_only": 1
}, },
{ {
@ -148,13 +148,13 @@
{ {
"fieldname": "additional_discount_percentage", "fieldname": "additional_discount_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Additional DIscount Percentage" "label": "Additional Discount Percentage"
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "additional_discount_amount", "fieldname": "additional_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Additional DIscount Amount" "label": "Additional Discount Amount"
}, },
{ {
"collapsible": 1, "collapsible": 1,
@ -267,7 +267,7 @@
"link_fieldname": "subscription" "link_fieldname": "subscription"
} }
], ],
"modified": "2023-09-18 17:48:21.900252", "modified": "2024-01-24 02:20:26.145996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@ -16,6 +16,7 @@ from frappe.utils.data import (
date_diff, date_diff,
flt, flt,
get_last_day, get_last_day,
get_link_to_form,
getdate, getdate,
nowdate, nowdate,
) )
@ -77,9 +78,7 @@ class Subscription(Document):
purchase_tax_template: DF.Link | None purchase_tax_template: DF.Link | None
sales_tax_template: DF.Link | None sales_tax_template: DF.Link | None
start_date: DF.Date | None start_date: DF.Date | None
status: DF.Literal[ status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"]
"", "Trialling", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"
]
submit_invoice: DF.Check submit_invoice: DF.Check
trial_period_end: DF.Date | None trial_period_end: DF.Date | None
trial_period_start: DF.Date | None trial_period_start: DF.Date | None
@ -232,7 +231,7 @@ class Subscription(Document):
Sets the status of the `Subscription` Sets the status of the `Subscription`
""" """
if self.is_trialling(): if self.is_trialling():
self.status = "Trialling" self.status = "Trialing"
elif ( elif (
self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date) self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
): ):
@ -317,6 +316,37 @@ class Subscription(Document):
if self.is_new(): if self.is_new():
self.set_subscription_status() self.set_subscription_status()
self.validate_party_billing_currency()
def validate_party_billing_currency(self):
"""
Subscription should be of the same currency as the Party's default billing currency or company default.
"""
if self.party:
party_billing_currency = frappe.get_cached_value(
self.party_type, self.party, "default_currency"
) or frappe.get_cached_value("Company", self.company, "default_currency")
plans = [x.plan for x in self.plans]
subscription_plan_currencies = frappe.db.get_all(
"Subscription Plan", filters={"name": ("in", plans)}, fields=["name", "currency"]
)
unsupported_plans = []
for x in subscription_plan_currencies:
if x.currency != party_billing_currency:
unsupported_plans.append("{0}".format(get_link_to_form("Subscription Plan", x.name)))
if unsupported_plans:
unsupported_plans = [
_(
"Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}"
).format(frappe.bold(party_billing_currency))
] + unsupported_plans
frappe.throw(
unsupported_plans, frappe.ValidationError, "Unsupported Subscription Plans", as_list=True
)
def validate_trial_period(self) -> None: def validate_trial_period(self) -> None:
""" """
Runs sanity checks on trial period dates for the `Subscription` Runs sanity checks on trial period dates for the `Subscription`
@ -356,18 +386,20 @@ class Subscription(Document):
self, self,
from_date: Optional[Union[str, datetime.date]] = None, from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None, to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document: ) -> Document:
""" """
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`. saves the `Subscription`.
Backwards compatibility Backwards compatibility
""" """
return self.create_invoice(from_date=from_date, to_date=to_date) return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date)
def create_invoice( def create_invoice(
self, self,
from_date: Optional[Union[str, datetime.date]] = None, from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None, to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document: ) -> Document:
""" """
Creates a `Invoice`, submits it and returns it Creates a `Invoice`, submits it and returns it
@ -385,11 +417,13 @@ class Subscription(Document):
invoice = frappe.new_doc(self.invoice_document_type) invoice = frappe.new_doc(self.invoice_document_type)
invoice.company = company invoice.company = company
invoice.set_posting_time = 1 invoice.set_posting_time = 1
invoice.posting_date = (
self.current_invoice_start if self.generate_invoice_at == "Beginning of the current subscription period":
if self.generate_invoice_at == "Beginning of the current subscription period" invoice.posting_date = self.current_invoice_start
else self.current_invoice_end elif self.generate_invoice_at == "Days before the current subscription period":
) invoice.posting_date = posting_date or self.current_invoice_start
else:
invoice.posting_date = self.current_invoice_end
invoice.cost_center = self.cost_center invoice.cost_center = self.cost_center
@ -413,6 +447,7 @@ class Subscription(Document):
# Subscription is better suited for service items. I won't update `update_stock` # Subscription is better suited for service items. I won't update `update_stock`
# for that reason # for that reason
items_list = self.get_items_from_plans(self.plans, is_prorate()) items_list = self.get_items_from_plans(self.plans, is_prorate())
for item in items_list: for item in items_list:
item["cost_center"] = self.cost_center item["cost_center"] = self.cost_center
invoice.append("items", item) invoice.append("items", item)
@ -556,8 +591,10 @@ class Subscription(Document):
if not self.is_current_invoice_generated( if not self.is_current_invoice_generated(
self.current_invoice_start, self.current_invoice_end self.current_invoice_start, self.current_invoice_end
) and self.can_generate_new_invoice(posting_date): ) and self.can_generate_new_invoice(posting_date):
self.generate_invoice() self.generate_invoice(posting_date=posting_date)
self.update_subscription_period(add_days(self.current_invoice_end, 1)) self.update_subscription_period(add_days(self.current_invoice_end, 1))
elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end):
self.update_subscription_period()
if self.cancel_at_period_end and ( if self.cancel_at_period_end and (
getdate(posting_date) >= getdate(self.current_invoice_end) getdate(posting_date) >= getdate(self.current_invoice_end)
@ -676,7 +713,7 @@ class Subscription(Document):
to_generate_invoice = ( to_generate_invoice = (
True True
if self.status == "Active" if self.status == "Active"
and not self.generate_invoice_at == "Beginning of the current subscription period" and self.generate_invoice_at != "Beginning of the current subscription period"
else False else False
) )
self.status = "Cancelled" self.status = "Cancelled"
@ -694,7 +731,7 @@ class Subscription(Document):
subscription and the `Subscription` will lose all the history of generated invoices subscription and the `Subscription` will lose all the history of generated invoices
it has. it has.
""" """
if not self.status == "Cancelled": if self.status != "Cancelled":
frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled) frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled)
self.status = "Active" self.status = "Active"

View File

@ -1,7 +1,7 @@
frappe.listview_settings['Subscription'] = { frappe.listview_settings['Subscription'] = {
get_indicator: function(doc) { get_indicator: function(doc) {
if(doc.status === 'Trialling') { if(doc.status === 'Trialing') {
return [__("Trialling"), "green"]; return [__("Trialing"), "green"];
} else if(doc.status === 'Active') { } else if(doc.status === 'Active') {
return [__("Active"), "green"]; return [__("Active"), "green"];
} else if(doc.status === 'Completed') { } else if(doc.status === 'Completed') {

View File

@ -46,7 +46,7 @@ class TestSubscription(FrappeTestCase):
get_date_str(subscription.current_invoice_end), get_date_str(subscription.current_invoice_end),
) )
self.assertEqual(subscription.invoices, []) self.assertEqual(subscription.invoices, [])
self.assertEqual(subscription.status, "Trialling") self.assertEqual(subscription.status, "Trialing")
def test_create_subscription_without_trial_with_correct_period(self): def test_create_subscription_without_trial_with_correct_period(self):
subscription = create_subscription() subscription = create_subscription()
@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase):
self.assertEqual(len(subscription.invoices), 1) self.assertEqual(len(subscription.invoices), 1)
def test_multi_currency_subscription(self): def test_multi_currency_subscription(self):
party = "_Test Subscription Customer"
frappe.db.set_value("Customer", party, "default_currency", "USD")
subscription = create_subscription( subscription = create_subscription(
start_date="2018-01-01", start_date="2018-01-01",
generate_invoice_at="Beginning of the current subscription period", generate_invoice_at="Beginning of the current subscription period",
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
party="_Test Subscription Customer", party=party,
) )
subscription.process() subscription.process()
@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase):
def make_plans(): def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900) create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
create_plan(plan_name="_Test Plan Name 2", cost=1999) create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR")
create_plan( create_plan(
plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 plan_name="_Test Plan Name 3",
cost=1999,
billing_interval="Day",
billing_interval_count=14,
currency="INR",
) )
create_plan( create_plan(
plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 plan_name="_Test Plan Name 4",
cost=20000,
billing_interval="Month",
billing_interval_count=3,
currency="INR",
) )
create_plan( create_plan(
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"

View File

@ -41,7 +41,8 @@
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency" "options": "Currency",
"reqd": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
@ -148,10 +149,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-12-10 15:24:15.794477", "modified": "2024-01-14 17:59:34.687977",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription Plan", "name": "Subscription Plan",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -193,5 +195,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -24,7 +24,7 @@ class SubscriptionPlan(Document):
billing_interval_count: DF.Int billing_interval_count: DF.Int
cost: DF.Currency cost: DF.Currency
cost_center: DF.Link | None cost_center: DF.Link | None
currency: DF.Link | None currency: DF.Link
item: DF.Link item: DF.Link
payment_gateway: DF.Link | None payment_gateway: DF.Link | None
plan_name: DF.Data plan_name: DF.Data

Some files were not shown because too many files have changed in this diff Show More